Skip to content

Commit

Permalink
Implement time zone support for conversion to datetime classes
Browse files Browse the repository at this point in the history
Closes #4.
  • Loading branch information
wbolster committed Oct 24, 2014
1 parent 1632dcc commit 5538234
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 41 deletions.
4 changes: 4 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ converted to the standard date and time classes using the
>>> moment.time()
datetime.time(18, 45, 23, 612883)

Conversion to and from classes from the ``datetime`` module have full time zone
support. See the API docs for :py:meth:`Moment.datetime` for more details about
time zone handling.

.. warning::

The Python ``temporenc`` module only concerns itself with encoding and
Expand Down
115 changes: 81 additions & 34 deletions temporenc/temporenc.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ def dst(self, dt):
def __repr__(self):
return '<{}>'.format(self._name)

UTC = _FixedOffset(0)


#
# Public API
Expand Down Expand Up @@ -298,7 +300,7 @@ def __le__(self, other):
def __hash__(self):
return hash(self._struct)

def datetime(self, strict=True):
def datetime(self, strict=True, local=False):
"""
Convert this value to a ``datetime.datetime`` instance.
Expand All @@ -316,69 +318,114 @@ def datetime(self, strict=True):
any application logic, but at least this allows applications to
use things like ``.strftime()`` on partial dates and times.
The *temporenc* format allows supports inclusion of a time zone
offset. Date and time information in the *temporenc* types
``DTZ`` and ``DTSZ`` is always stored as UTC, but the original
UTC offset is included, which makes conversion to the original
local time possible. When converting to a ``datetime`` instance,
time zone information is handled as follows:
* When no time zone information was present in the original data
(e.g. when unpacking *temporenc* type ``DT``), the return
value will be a naive `datetime` instance, i.e. its ``tzinfo``
attribute is `None`.
* If the original data did include time zone information, the
return value will be a time zone aware instance. No conversion
to local time is performed by default, which means the
instance will have a ``tzinfo`` attribute corresponding to
UTC. This means time zone information will be lost, and the
return value will be in UTC.
If this is not desired, i.e. the application wants to access
the original local time, set the `local` argument to `True`.
In that case the data will be converted to local time, and the
return value will have a ``tzinfo`` attribute corresponding to
the time zone offset.
:param bool strict: whether to use strict conversion rules
:param bool local: whether to convert to local time
:return: converted value
:type: `datetime.datetime`
"""
# FIXME: this indirect construction is a bit slow...
return datetime.datetime.combine(
self.date(strict=strict),
self.time(strict=strict))

def date(self, strict=True):
if strict:
if None in (self.year, self.month, self.day):
raise ValueError("incomplete date information")
if None in (self.hour, self.minute, self.second):
raise ValueError("incomplete time information")

hour, minute, second = self.hour, self.minute, self.second
year, month, day = self.year, self.month, self.day

# The stdlib's datetime classes always specify microseconds.
us = self.microsecond if self.microsecond is not None else 0

if not strict:
# Substitute defaults for missing values.
if year is None:
year = 1

if month is None:
month = 1

if day is None:
day = 1

if hour is None:
hour = 0

if minute is None:
minute = 0

if second is None:
second = 0
elif second == 60: # assume that this is a leap second
second = 59

dt = datetime.datetime(
year, month, day,
hour, minute, second, us,
tzinfo=None if self.tz_offset is None else UTC)

if local and dt.tzinfo is not None:
dt = dt.astimezone(_FixedOffset(self.tz_offset))

return dt

def date(self, strict=True, local=False):
"""
Convert this value to a ``datetime.date`` instance.
See the documentation for the :py:meth:`datetime()` method for
more information.
:param bool strict: whether to use strict conversion rules
:param bool local: whether to convert to local time
:return: converted value
:type: `datetime.date`
"""
if not strict:
return datetime.date(
self.year if self.year is not None else 1,
self.month if self.month is not None else 1,
self.day if self.day is not None else 1)

if None in (self.year, self.month, self.day):
if strict and None in (self.year, self.month, self.day):
raise ValueError("incomplete date information")

return datetime.date(self.year, self.month, self.day)
return self.datetime(strict=False, local=local).date()

def time(self, strict=True):
def time(self, strict=True, local=False):
"""
Convert this value to a ``datetime.time`` instance.
See the documentation for the :py:meth:`datetime()` method for
more information.
:param bool strict: whether to use strict conversion rules
:param bool local: whether to convert to local time
:return: converted value
:type: `datetime.date`
"""
# The stdlib's datetime classes always specify microseconds.
us = self.microsecond if self.microsecond is not None else 0

if not strict:
if self.second is None:
second = 0
elif self.second == 60:
# Assumption: this is a leap second
second = 59
else:
second = self.second

return datetime.time(
self.hour if self.hour is not None else 0,
self.minute if self.minute is not None else 0,
second, us)

if None in (self.hour, self.minute, self.second):
if strict and None in (self.hour, self.minute, self.second):
raise ValueError("incomplete time information")

return datetime.time(self.hour, self.minute, self.second, us)
return self.datetime(strict=False, local=local).time()


def packb(
Expand Down
41 changes: 34 additions & 7 deletions tests/test_temporenc.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,27 +378,54 @@ def test_native_packing():
assert actual == expected


def test_native_packing_time_zone():
def test_native_time_zone():

# Python < 3.2 doesn't have concrete tzinfo implementations, so
# use the internal helper class instead to avoid depending on newer
# Python versions (or on pytz).
# Python < 3.2 doesn't have concrete tzinfo implementations. This
# test uses the internal helper class instead to avoid depending on
# newer Python versions (or on pytz).
from temporenc.temporenc import _FixedOffset
tz = _FixedOffset(60) # UTC +01:00

dutch_winter = _FixedOffset(60) # UTC +01:00

# DTZ
actual = temporenc.packb(
datetime.datetime(1983, 1, 15, 18, 25, 12, 0, tzinfo=tz),
datetime.datetime(1983, 1, 15, 18, 25, 12, 0, tzinfo=dutch_winter),
type='DTZ')
expected = from_hex('cf 7e 0e 8b 26 44')
assert actual == expected
moment = temporenc.unpackb(expected)
assert moment.hour == 17 # internal fields are in UTC
assert moment.tz_offset == 60 # tz_offset is stored alongside
assert (moment.tz_hour, moment.tz_minute) == (1, 0)
as_utc = moment.datetime()
assert as_utc.hour == 17
assert as_utc.utcoffset() == datetime.timedelta(0)
as_local = moment.datetime(local=True)
assert as_local.hour == 18
assert as_local.utcoffset() == datetime.timedelta(minutes=60)

# DTSZ (microsecond, since native types have that precision)
actual = temporenc.packb(
datetime.datetime(1983, 1, 15, 18, 25, 12, 123456, tzinfo=tz),
datetime.datetime(
1983, 1, 15, 18, 25, 12, 123456,
tzinfo=dutch_winter),
type='DTSZ')
dtsz_us = from_hex('eb df 83 a2 c9 83 c4 81 10')
assert actual == dtsz_us
moment = temporenc.unpackb(expected)
assert moment.datetime().hour == 17
assert moment.datetime(local=True).hour == 18

# Year transition with time zones
moment = temporenc.unpackb(temporenc.packb(
datetime.datetime(2014, 1, 1, 0, 30, 0, tzinfo=dutch_winter),
type='DTZ'))
assert moment.date().year == 2013
assert moment.datetime().year == 2013
assert moment.date(local=True).year == 2014
assert moment.datetime(local=True).year == 2014
assert moment.time().hour == 23
assert moment.time(local=True).hour == 0


def test_native_packing_with_overrides():
Expand Down

0 comments on commit 5538234

Please sign in to comment.