import datetime
import functools
from typing import Tuple
from . import date
from .impl.util import MICROSECONDS_PER_SECOND, MICROSECONDS_PER_DAY, c_modulus, c_divide
from .impl import hapi
[docs]
@functools.total_ordering
class Timestamp:
"""
A Hyper timestamp - date and time of day.
This class is similar to ``datetime.datetime``. The difference is that it supports a greater range of dates: while
Python years range from 1 to 9999, Hyper supports years from ``-4712`` (4713 BC) to ``294276``.
:param year: Year.
:param month: Month, from 1 to 12.
:param day: Day, from 1 to the number of days in the month.
:param hour: Optional hour value, from 0 to 23.
:param minute: Optional minute value, from 0 to 59.
:param second: Optional second value, from 0 to 59.
:param microsecond: Optional microsecond value, from 0 to 999_999.
:param tzinfo: Optional tzinfo value, may be None or an instance of a tzinfo subclass.
.. testsetup:: Timestamp.__init__
from tableauhyperapi import *
.. doctest:: Timestamp.__init__
>>> print(Timestamp(2019, 6, 13))
2019-06-13 00:00:00
>>> print(Timestamp(2019, 6, 13, 13, 23, 45))
2019-06-13 13:23:45
>>> print(Timestamp(2019, 6, 13, 13, 23, 45, 789876))
2019-06-13 13:23:45.789876
"""
MINYEAR = -4712
""" The earliest representable year. """
MAXYEAR = 294276
""" The latest representable year. """
def __init__(self, *args, tzinfo=None):
self.__tzinfo = tzinfo
if len(args) == 1:
self._hyper_value = args[0]
return
if len(args) == 3:
vd = hapi.hyper_encode_date(args)
vt = 0
elif len(args) == 6:
vd = hapi.hyper_encode_date(args[:3])
vt = hapi.hyper_encode_time(args[3:] + (0,))
elif len(args) == 7:
vd = hapi.hyper_encode_date(args[:3])
vt = hapi.hyper_encode_time(args[3:])
else:
raise ValueError('Invalid number of arguments, expected 3, 6, or 7')
self._hyper_value = vd * MICROSECONDS_PER_DAY + vt
[docs]
@staticmethod
def today() -> 'Timestamp':
""" Returns the current local date. """
return Timestamp.from_datetime(datetime.datetime.today())
[docs]
@staticmethod
def now(tz=None) -> 'Timestamp':
"""
Returns the current local date and time. If `tz` is not `None`, it must be an instance of a `tzinfo`
subclass, and the current date and time are converted to `tz`'s time zone.
"""
return Timestamp.from_datetime(datetime.datetime.now(tz))
[docs]
@staticmethod
def from_date(value: [datetime.date, 'date.Date']) -> 'Timestamp':
""" Converts a Python ``datetime.date`` or :any:`Date` value to a :any:`Timestamp`. """
return Timestamp(value.year, value.month, value.day)
[docs]
def date(self) -> 'Timestamp':
""" Gets the date part of this value. """
year, month, day = self.__decode_date()
return Timestamp(year, month, day)
[docs]
def to_date(self) -> 'date.Date':
""" Gets the date part of this value as a :any:`Date`. """
year, month, day = self.__decode_date()
return date.Date(year, month, day)
[docs]
@staticmethod
def from_datetime(value: datetime.datetime) -> 'Timestamp':
""" Converts a Python ``datetime.datetime`` value to a :any:`Timestamp`. """
return Timestamp(value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond,
tzinfo=value.tzinfo)
[docs]
def to_datetime(self) -> datetime.datetime:
""" Converts this value to Python ``datetime.datetime``. """
year, month, day = self.__decode_date()
hour, minute, second, microsecond = self.__decode_time()
return datetime.datetime(year, month, day, hour, minute, second, microsecond, self.__tzinfo)
def __to_datetime_restrict(self) -> datetime.datetime:
"""
Converts this value to Python ``datetime.datetime``. When the `year` component is outside of the range
datetime.MINYEAR..datetime.MAXYEAR, it is restricted to this range.
"""
year, month, day = self.__decode_date()
hour, minute, second, microsecond = self.__decode_time()
if year > datetime.MAXYEAR:
year = datetime.MAXYEAR
elif year < datetime.MINYEAR:
year = datetime.MINYEAR
return datetime.datetime(year, month, day, hour, minute, second, microsecond, tzinfo=self.__tzinfo)
def __decode_date(self) -> Tuple[int, int, int]:
comps = hapi.hyper_decode_date(c_divide(self._hyper_value, MICROSECONDS_PER_DAY))
return comps.year, comps.month, comps.day
def __decode_time(self) -> Tuple[int, int, int, int]:
comps = hapi.hyper_decode_time(c_modulus(self._hyper_value, MICROSECONDS_PER_DAY))
return comps.hour, comps.minute, comps.second, comps.microsecond
[docs]
def utcoffset(self) -> datetime.timedelta:
"""
If tzinfo is None, returns None. Otherwise, returns the timedelta returned by self.tzinfo.utcoffset(datetime).
`datetime` is obtained by converting the timestamp to a datetime restricting the year to the range
datetime.MINYEAR..datetime.MAXYEAR. This method raises an exception if self.tzinfo.utcoffset(datetime) doesn’t
return None or a timedelta object representing a whole number of minutes with magnitude less than one day.
"""
return self.__to_datetime_restrict().utcoffset()
def _format_utcoffset(self):
""" Format the UTC offset in the format '+HH:MM[:SS]' """
utcoffset = self.utcoffset()
if utcoffset is not None:
sign = '-' if utcoffset.days < 0 else '+'
total = abs(utcoffset.seconds + utcoffset.days * 24 * 60 * 60)
tz_seconds = total % 60
total //= 60
tz_minutes = total % 60
total //= 60
tz_hours = total
if tz_seconds == 0:
return f'{sign}{tz_hours:02}:{tz_minutes:02}'
else:
return f'{sign}{tz_hours:02}:{tz_minutes:02}:{tz_seconds:02}'
return ''
@staticmethod
def __timedelta_to_microseconds(v: datetime.timedelta):
return v.days * MICROSECONDS_PER_DAY + v.seconds * MICROSECONDS_PER_SECOND + v.microseconds
[docs]
def astimezone(self, tz=None):
"""
Return a Timestamp object with new tzinfo attribute tz, adjusting the date and time data so the result is the
same UTC time as self, but in tz’s local time.
If provided, tz must be an instance of a tzinfo subclass, and its utcoffset() and dst() methods must not return
None. If self is naive, it is presumed to represent time in the system timezone.
If called without arguments (or with tz=None) the system local timezone is assumed for the target timezone. The
.tzinfo attribute of the converted timestamp instance will be set to an instance of timezone with the zone name
and offset obtained from the OS.
"""
dt = self.__to_datetime_restrict()
current_tz = self.__tzinfo if self.__tzinfo is not None else dt.astimezone().tzinfo
new_tz = tz if tz is not None else dt.astimezone().tzinfo
current_offset = Timestamp.__timedelta_to_microseconds(current_tz.utcoffset(dt))
new_offset = Timestamp.__timedelta_to_microseconds(new_tz.utcoffset(dt))
return Timestamp(self._hyper_value - current_offset + new_offset, tzinfo=new_tz)
@property
def year(self) -> int:
""" Gets the year part of this value. """
year, month, day = self.__decode_date()
return year
@property
def month(self) -> int:
""" Gets the month part of this value, as a number between 1 and 12. """
year, month, day = self.__decode_date()
return month
@property
def day(self) -> int:
""" Gets the day part of this value, as a number between 1 and the number of days in the month. """
year, month, day = self.__decode_date()
return day
@property
def hour(self) -> int:
""" Gets the hour part of this value, as a number between 0 and 23. """
hour, minute, second, microsecond = self.__decode_time()
return hour
@property
def minute(self) -> int:
""" Gets the minute part of this value, as a number between 0 and 59. """
hour, minute, second, microsecond = self.__decode_time()
return minute
@property
def second(self) -> int:
""" Gets the second part of this value, as an integer between 0 and 59. """
hour, minute, second, microsecond = self.__decode_time()
return second
@property
def microsecond(self) -> int:
""" Gets the second part of this value, as an integer between 0 and 999999. """
hour, minute, second, microsecond = self.__decode_time()
return microsecond
@property
def tzinfo(self) -> datetime.tzinfo:
""" Gets the tzinfo of this value, may be None or an instance of a tzinfo subclass. """
return self.__tzinfo
def __repr__(self):
year, month, day = self.__decode_date()
hour, minute, second, microsecond = self.__decode_time()
return f'Timestamp({year}, {month}, {day}, {hour}, {minute}, {second}, {microsecond}, {self.__tzinfo})'
def __str__(self):
year, month, day = self.__decode_date()
hour, minute, second, microsecond = self.__decode_time()
offset = self._format_utcoffset()
if microsecond == 0:
return f'{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}{offset}'
else:
return f'{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}.{microsecond:06}{offset}'
def __eq__(self, other):
if isinstance(other, Timestamp):
# both naive or both aware with the same tzinfo
if self.__tzinfo is other.__tzinfo:
return self._hyper_value == other._hyper_value
offset1 = self.utcoffset()
offset2 = other.utcoffset()
# both naive
if offset1 is None and offset2 is None:
return self._hyper_value == other._hyper_value
# one naive, one aware
if offset1 is None or offset2 is None:
return False
# both aware with different tzinfo
v1 = self._hyper_value - Timestamp.__timedelta_to_microseconds(offset1)
v2 = other._hyper_value - Timestamp.__timedelta_to_microseconds(offset2)
return v1 == v2
return NotImplemented
def __lt__(self, other):
if isinstance(other, Timestamp):
# both naive or both aware with the same tzinfo
if self.__tzinfo is other.__tzinfo:
return self._hyper_value < other._hyper_value
offset1 = self.utcoffset()
offset2 = other.utcoffset()
# both naive
if offset1 is None and offset2 is None:
return self._hyper_value < other._hyper_value
# one naive, one aware
if offset1 is None or offset2 is None:
raise TypeError("can't compare offset-naive and offset-aware timestamps")
# both aware with different tzinfo
v1 = self._hyper_value - Timestamp.__timedelta_to_microseconds(offset1)
v2 = other._hyper_value - Timestamp.__timedelta_to_microseconds(offset2)
return v1 < v2
return NotImplemented
def __hash__(self):
return hash((self._hyper_value, self.__tzinfo))
def __add__(self, other):
if isinstance(other, datetime.timedelta):
return Timestamp(self._hyper_value + Timestamp.__timedelta_to_microseconds(other), tzinfo=self.__tzinfo)
return NotImplemented
def __sub__(self, other):
if isinstance(other, datetime.timedelta):
return Timestamp(self._hyper_value - Timestamp.__timedelta_to_microseconds(other), tzinfo=self.__tzinfo)
if isinstance(other, Timestamp):
# both naive or both aware with the same tzinfo
if self.__tzinfo is other.__tzinfo:
return datetime.timedelta(microseconds=self._hyper_value - other._hyper_value)
offset1 = self.utcoffset()
offset2 = other.utcoffset()
# both naive
if offset1 is None and offset2 is None:
return datetime.timedelta(microseconds=self._hyper_value - other._hyper_value)
# one naive, one aware
if offset1 is None or offset2 is None:
raise TypeError("can't subtract offset-naive and offset-aware timestamps")
# both aware with different tzinfo
v1 = self._hyper_value - Timestamp.__timedelta_to_microseconds(offset1)
v2 = other._hyper_value - Timestamp.__timedelta_to_microseconds(offset2)
return datetime.timedelta(microseconds=v1 - v2)
return NotImplemented