Commit 85c5932c authored by Laurence Rowe's avatar Laurence Rowe

The DateTime function may now be invoked with a single argument

that is a datetime.datetime instance. Timezone naive DateTimes may
be converted back to timezone naive datetime.datetime objects with
asdatetime(). All DateTime instances may be converted to a timezone
naive datetime.datetime in UTC with utcdatetime().
parent c00c2807
......@@ -49,11 +49,11 @@ except:
tzname=('UNKNOWN','UNKNOWN')
# To control rounding errors, we round system time to the nearest
# millisecond. Then delicate calculations can rely on that the
# microsecond. Then delicate calculations can rely on that the
# maximum precision that needs to be preserved is known.
_system_time = time
def time():
return round(_system_time(), 3)
return round(_system_time(), 6)
# Determine machine epoch
tm=((0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334),
......@@ -97,7 +97,7 @@ iso8601Match = re.compile(r'''
)? # after minute is optional
)? # after hour is optional
(?: # timezone:
Z # one Z
(?P<Z>Z) # one Z
| # or:
(?P<signal>[-+]) # one plus or one minus as signal
(?P<hour_off>\d # one digit for hour offset...
......@@ -373,10 +373,10 @@ def _calcIndependentSecondEtc(tz, x, ms):
x_adjusted = x - fset + ms
d = x_adjusted / 86400.0
t = x_adjusted - long(EPOCH) + 86400L
millis = (x + 86400 - fset) * 1000 + \
long(round(ms * 1000.0)) - long(EPOCH * 1000.0)
micros = (x + 86400 - fset) * 1000000 + \
long(round(ms * 1000000.0)) - long(EPOCH * 1000000.0)
s = d - math.floor(d)
return s,d,t,millis
return s,d,t,micros
def _calcHMS(x, ms):
# hours, minutes, seconds from integer and float.
......@@ -527,7 +527,8 @@ class DateTime:
local machine timezone). DateTime objects also provide access
to their value in a float format usable with the python time
module, provided that the value of the object falls in the
range of the epoch-based time module.
range of the epoch-based time module, and as a datetime.datetime
object.
A DateTime object should be considered immutable; all conversion
and numeric operations return a new DateTime object rather than
......@@ -666,6 +667,13 @@ class DateTime:
that is a DateTime instane, a copy of the passed object will
be created.
- New in 2.11:
The DateTime function may now be invoked with a single argument
that is a datetime.datetime instance. Timezone naive DateTimes may
be converted back to timezone naive datetime.datetime objects with
asdatetime(). All DateTime instances may be converted to a timezone
naive datetime.datetime in UTC with utcdatetime().
- If the function is invoked with two numeric arguments, then
the first is taken to be an integer year and the second
argument is taken to be an offset in days from the beginning
......@@ -738,14 +746,23 @@ class DateTime:
datefmt = kw.get('datefmt', getDefaultDateFormat())
d=t=s=None
ac=len(args)
millisecs = None
microsecs = None
if ac==10:
# Internal format called only by DateTime
yr,mo,dy,hr,mn,sc,tz,t,d,s=args
elif ac == 11:
# Internal format that includes milliseconds.
# Internal format that includes milliseconds (from the epoch)
yr,mo,dy,hr,mn,sc,tz,t,d,s,millisecs=args
microsecs = millisecs * 1000
elif ac == 12:
# Internal format that includes microseconds (from the epoch) and a
# flag indicating whether this was constructed in a timezone naive
# manner
yr,mo,dy,hr,mn,sc,tz,t,d,s,microsecs,tznaive=args
if tznaive is not None: # preserve this information
self._timezone_naive = tznaive
elif not args or (ac and args[0]==None):
# Current time, to be displayed in local timezone
......@@ -756,6 +773,7 @@ class DateTime:
s,d = _calcSD(t)
yr,mo,dy,hr,mn,sc=lt[:6]
sc=sc+ms
self._timezone_naive = False
elif ac==1:
arg=args[0]
......@@ -775,6 +793,22 @@ class DateTime:
yr,mo,dy,hr,mn,sc=lt[:6]
sc=sc+ms
elif isinstance(arg, datetime):
yr,mo,dy,hr,mn,sc,tz,tznaive=self._parse_iso8601_preserving_tznaive(arg.isoformat())
self._timezone_naive = tznaive
ms = sc - math.floor(sc)
x = _calcDependentSecond2(yr,mo,dy,hr,mn,sc)
if tz:
try: tz=self._tzinfo._zmap[tz.lower()]
except KeyError:
if numericTimeZoneMatch(tz) is None:
raise DateTimeError, \
'Unknown time zone in date: %s' % arg
else:
tz = self._calcTimezoneName(x, ms)
s,d,t,microsecs = _calcIndependentSecondEtc(tz, x, ms)
elif isinstance(arg, (unicode, str)) and arg.lower() in self._tzinfo._zidx:
# Current time, to be displayed in specified timezone
t,tz=time(),self._tzinfo._zmap[arg.lower()]
......@@ -784,13 +818,15 @@ class DateTime:
x = _calcDependentSecond(tz, t)
yr,mo,dy,hr,mn,sc = _calcYMDHMS(x, ms)
elif isinstance(arg, (unicode, str)):
# Date/time string
iso8601 = iso8601Match(arg.strip())
fields_iso8601 = iso8601 and iso8601.groupdict() or {}
if fields_iso8601 and not fields_iso8601.get('garbage'):
yr,mo,dy,hr,mn,sc,tz=self._parse_iso8601(arg)
yr,mo,dy,hr,mn,sc,tz,tznaive=self._parse_iso8601_preserving_tznaive(arg)
self._timezone_naive = tznaive
else:
yr,mo,dy,hr,mn,sc,tz=self._parse(arg, datefmt)
......@@ -809,7 +845,7 @@ class DateTime:
'Unknown time zone in date: %s' % arg
else:
tz = self._calcTimezoneName(x, ms)
s,d,t,millisecs = _calcIndependentSecondEtc(tz, x, ms)
s,d,t,microsecs = _calcIndependentSecondEtc(tz, x, ms)
else:
# Seconds from epoch, gmt
......@@ -844,7 +880,7 @@ class DateTime:
ms = x_float - x_floor
x = long(x_floor)
yr,mo,dy,hr,mn,sc = _calcYMDHMS(x, ms)
s,d,t,millisecs = _calcIndependentSecondEtc(tz, x, ms)
s,d,t,microsecs = _calcIndependentSecondEtc(tz, x, ms)
else:
# Explicit format
yr,mo,dy=args[:3]
......@@ -878,7 +914,7 @@ class DateTime:
else:
# Get local time zone name
tz = self._calcTimezoneName(x, ms)
s,d,t,millisecs = _calcIndependentSecondEtc(tz, x, ms)
s,d,t,microsecs = _calcIndependentSecondEtc(tz, x, ms)
if hr>12:
self._pmhour=hr-12
......@@ -891,24 +927,25 @@ class DateTime:
self._months[mo],self._months_a[mo],self._months_p[mo]
self._fday,self._aday,self._pday= \
self._days[dx],self._days_a[dx],self._days_p[dx]
# Round to nearest millisecond in platform-independent way. You
# Round to nearest microsecond in platform-independent way. You
# cannot rely on C sprintf (Python '%') formatting to round
# consistently; doing it ourselves ensures that all but truly
# horrid C sprintf implementations will yield the same result
# x-platform, provided the format asks for exactly 3 digits after
# x-platform, provided the format asks for exactly 6 digits after
# the decimal point.
sc = round(sc, 3)
if sc >= 60.0: # can happen if, e.g., orig sc was 59.9999
sc = 59.999
sc = round(sc, 6)
if sc >= 60.0: # can happen if, e.g., orig sc was 59.9999999
sc = 59.999999
self._nearsec=math.floor(sc)
self._year,self._month,self._day =yr,mo,dy
self._hour,self._minute,self._second =hr,mn,sc
self.time,self._d,self._t,self._tz =s,d,t,tz
if millisecs is None:
millisecs = long(math.floor(t * 1000.0))
self._millis = millisecs
# self._millis is the time since the epoch
# in long integer milliseconds.
if microsecs is None:
microsecs = long(math.floor(t * 1000000.0))
self._micros = microsecs
# self._micros is the time since the epoch
# in long integer microseconds.
int_pattern =re.compile(r'([0-9]+)') #AJ
flt_pattern =re.compile(r':([0-9]+\.[0-9]+)') #AJ
......@@ -1019,8 +1056,10 @@ class DateTime:
sp=st.split()
tz=sp[-1]
if tz and (tz.lower() in ValidZones):
self._timezone_naive = False
st=' '.join(sp[:-1])
else:
self._timezone_naive = True
tz = None # Decide later, since the default time zone
# could depend on the date.
......@@ -1219,14 +1258,15 @@ class DateTime:
object, represented in the indicated timezone.
"""
t,tz=self._t,self._tzinfo._zmap[z.lower()]
millis = self.millis()
micros = self.micros()
tznaive = False # you're performing a timzone change, can't be naive
try:
# Try to use time module for speed.
yr,mo,dy,hr,mn,sc=safegmtime(t+_tzoffset(tz, t))[:6]
sc=self._second
return self.__class__(yr,mo,dy,hr,mn,sc,tz,t,
self._d,self.time,millis)
self._d,self.time,micros,tznaive)
except: # gmtime can't perform the calculation in the given range.
# Calculate the difference between the two time zones.
tzdiff = _tzoffset(tz, t) - _tzoffset(self._tz, t)
......@@ -1239,7 +1279,7 @@ class DateTime:
x_new = x + tzdiff
yr,mo,dy,hr,mn,sc = _calcYMDHMS(x_new, ms)
return self.__class__(yr,mo,dy,hr,mn,sc,tz,t,
self._d,self.time,millis)
self._d,self.time,micros,tznaive)
def isFuture(self):
"""Return true if this object represents a date/time
......@@ -1324,13 +1364,13 @@ class DateTime:
than the specified DateTime or time module style time.
Revised to give more correct results through comparison of
long integer milliseconds.
long integer microseconds.
"""
# Optimized for sorting speed
try:
return (self._millis > t._millis)
return (self._micros > t._micros)
except AttributeError:
try: self._millis
try: self._micros
except AttributeError: self._upgrade_old()
return (self._t > t)
......@@ -1346,13 +1386,13 @@ class DateTime:
time.
Revised to give more correct results through comparison of
long integer milliseconds.
long integer microseconds.
"""
# Optimized for sorting speed
try:
return (self._millis >= t._millis)
return (self._micros >= t._micros)
except AttributeError:
try: self._millis
try: self._micros
except AttributeError: self._upgrade_old()
return (self._t >= t)
......@@ -1367,13 +1407,13 @@ class DateTime:
the specified DateTime or time module style time.
Revised to give more correct results through comparison of
long integer milliseconds.
long integer microseconds.
"""
# Optimized for sorting speed
try:
return (self._millis == t._millis)
return (self._micros == t._micros)
except AttributeError:
try: self._millis
try: self._micros
except AttributeError: self._upgrade_old()
return (self._t == t)
......@@ -1388,13 +1428,13 @@ class DateTime:
to the specified DateTime or time module style time.
Revised to give more correct results through comparison of
long integer milliseconds.
long integer microseconds.
"""
# Optimized for sorting speed
try:
return (self._millis != t._millis)
return (self._micros != t._micros)
except AttributeError:
try: self._millis
try: self._micros
except AttributeError: self._upgrade_old()
return (self._t != t)
......@@ -1409,13 +1449,13 @@ class DateTime:
the specified DateTime or time module style time.
Revised to give more correct results through comparison of
long integer milliseconds.
long integer microseconds.
"""
# Optimized for sorting speed
try:
return (self._millis < t._millis)
return (self._micros < t._micros)
except AttributeError:
try: self._millis
try: self._micros
except AttributeError: self._upgrade_old()
return (self._t < t)
......@@ -1430,13 +1470,13 @@ class DateTime:
or equal to the specified DateTime or time module style time.
Revised to give more correct results through comparison of
long integer milliseconds.
long integer microseconds.
"""
# Optimized for sorting speed
try:
return (self._millis <= t._millis)
return (self._micros <= t._micros)
except AttributeError:
try: self._millis
try: self._micros
except AttributeError: self._upgrade_old()
return (self._t <= t)
......@@ -1559,15 +1599,35 @@ class DateTime:
def millis(self):
"""Return the millisecond since the epoch in GMT."""
try:
return self._millis
micros = self._micros
except AttributeError:
micros = self._upgrade_old()
return micros / 1000
def micros(self):
"""Return the microsecond since the epoch in GMT."""
try:
return self._micros
except AttributeError:
return self._upgrade_old()
def timezoneNaive(self):
"""The python datetime module introduces the idea of distinguishing
between timezone aware and timezone naive datetime values. For lossless
conversion to and from datetime.datetime record if we record this
information using True / False. DateTime makes no distinction, when we
don't have any information we return None here.
"""
try:
return self._timezone_naive
except AttributeError:
return None
def _upgrade_old(self):
"""Upgrades a previously pickled DateTime object."""
millis = long(math.floor(self._t * 1000.0))
self._millis = millis
return millis
micros = long(math.floor(self._t * 1000000.0))
#self._micros = micros # don't upgrade instances in place
return micros
def strftime(self, format):
"""Format the date/time using the *current timezone representation*."""
......@@ -1713,9 +1773,16 @@ class DateTime:
T is a literal character.
TZD is Time Zone Designator, format +HH:MM or -HH:MM
If the instance is timezone naive (it was not specified with a timezone
when it was constructed) then the timezone is ommitted.
The HTML4 method below offers the same formatting, but converts
to UTC before returning the value and sets the TZD "Z".
"""
if self.timezoneNaive():
return "%0.4d-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d" % (
self._year, self._month, self._day,
self._hour, self._minute, self._second)
tzoffset = _tzoffset2iso8601zone(_tzoffset(self._tz, self._t))
return "%0.4d-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d%s" % (
self._year, self._month, self._day,
......@@ -1736,6 +1803,30 @@ class DateTime:
newdate._year, newdate._month, newdate._day,
newdate._hour, newdate._minute, newdate._second)
def asdatetime(self):
"""Return a standard libary datetime.datetime
"""
tznaive = self.timezoneNaive()
if tznaive is True:
# we were either converted from an ISO8601 timezone naive string or
# a timezone naive datetime
second = int(self._second)
microsec = self.micros() % 1000000
dt = datetime(self._year, self._month, self._day, self._hour,
self._minute, second, microsec)
return dt
else:
raise NotImplementedError('conversion of datetime aware DateTime to datetime unsupported')
def utcdatetime(self):
"""Convert the time to UTC then return a timezone naive datetime object"""
utc = self.toZone('UTC')
second = int(utc._second)
microsec = utc.micros() % 1000000
dt = datetime(utc._year, utc._month, utc._day, utc._hour,
utc._minute, second, microsec)
return dt
def __add__(self,other):
"""A DateTime may be added to a number and a number may be
added to a DateTime; two DateTimes cannot be added.
......@@ -1744,13 +1835,17 @@ class DateTime:
raise DateTimeError,'Cannot add two DateTimes'
o=float(other)
tz = self._tz
t = (self._t + (o*86400.0))
d = (self._d + o)
#t = (self._t + (o*86400.0))
omicros = round(o*86400000000)
tmicros = self.micros() + omicros
#d = (self._d + o)
t = tmicros / 1000000.0
d = (tmicros + long(EPOCH*1000000)) / 86400000000.0
s = d - math.floor(d)
ms = t - math.floor(t)
x = _calcDependentSecond(tz, t)
yr,mo,dy,hr,mn,sc = _calcYMDHMS(x, ms)
return self.__class__(yr,mo,dy,hr,mn,sc,self._tz,t,d,s)
return self.__class__(yr,mo,dy,hr,mn,sc,self._tz,t,d,s, None, self.timezoneNaive())
__radd__=__add__
......@@ -1760,11 +1855,7 @@ class DateTime:
a number.
"""
if hasattr(other, '_d'):
if 0: # This logic seems right but is incorrect.
my_t = self._t + _tzoffset(self._tz, self._t)
ob_t = other._t + _tzoffset(other._tz, other._t)
return (my_t - ob_t) / 86400.0
return self._d - other._d
return (self.micros() - other.micros()) / 86400000000.0
else:
return self.__add__(-(other))
......@@ -1786,10 +1877,10 @@ class DateTime:
return '%4.4d/%2.2d/%2.2d %2.2d:%2.2d:%2.2d %s' % (
y, m, d, h, mn, s, t)
else:
# s is already rounded to the nearest millisecond, and
# s is already rounded to the nearest microsecond, and
# it's not a whole number of seconds. Be sure to print
# 2 digits before the decimal point.
return '%4.4d/%2.2d/%2.2d %2.2d:%2.2d:%06.3f %s' % (
return '%4.4d/%2.2d/%2.2d %2.2d:%2.2d:%06.6f %s' % (
y, m, d, h, mn, s, t)
def __cmp__(self,obj):
......@@ -1806,9 +1897,9 @@ class DateTime:
"""
# Optimized for sorting speed.
try:
return cmp(self._millis, obj._millis)
return cmp(self._micros, obj._micros)
except AttributeError:
try: self._millis
try: self._micros
except AttributeError: self._upgrade_old()
return cmp(self._t,obj)
......@@ -1819,17 +1910,22 @@ class DateTime:
def __int__(self):
"""Convert to an integer number of seconds since the epoch (gmt)."""
return int(self.millis() / 1000)
return int(self.micros() / 1000000)
def __long__(self):
"""Convert to a long-int number of seconds since the epoch (gmt)."""
return long(self.millis() / 1000)
return long(self.micros() / 1000000)
def __float__(self):
"""Convert to floating-point number of seconds since the epoch (gmt)."""
return float(self._t)
def _parse_iso8601(self,s):
# preserve the previously implied contract
# who know where this could be used...
return _parse_iso8601_preserving_tznaive(s)[:7]
def _parse_iso8601_preserving_tznaive(self,s):
try:
return self.__parse_iso8601(s)
except IndexError:
......@@ -1839,10 +1935,11 @@ class DateTime:
def __parse_iso8601(self,s):
"""Parse an ISO 8601 compliant date.
See: http://www.omg.org/docs/ISO-stds/06-08-01.pdf
See: http://en.wikipedia.org/wiki/ISO_8601
"""
month = day = week_day = 1
year = hour = minute = seconds = hour_off = min_off = 0
tznaive = True
iso8601 = iso8601Match(s.strip())
fields = iso8601 and iso8601.groupdict() or {}
......@@ -1898,9 +1995,18 @@ class DateTime:
if fields['min_off']:
min_off = int(fields['min_off'])
if fields['signal'] or fields['Z']:
tznaive = False
tz = 'GMT%+03d%02d' % (hour_off, min_off)
else:
tznaive = True
# Figure out what time zone it is in the local area
# on the given date.
ms = seconds - math.floor(seconds)
x = _calcDependentSecond2(year,month,day,hour,minute,seconds)
tz = self._calcTimezoneName(x, ms)
return year, month, day, hour, minute, seconds, tz
return year, month, day, hour, minute, seconds, tz, tznaive
def JulianDay(self):
"""Return the Julian day.
......
......@@ -19,6 +19,8 @@ import unittest
from DateTime.DateTime import _findLocalTimeZoneName
from DateTime import DateTime
from datetime import datetime
import pytz
try:
__file__
......@@ -135,6 +137,7 @@ class DateTimeTests(unittest.TestCase):
def testSubtraction(self):
# Reconstruction of a DateTime from its parts, with subtraction
# this also tests the accuracy of addition and reconstruction
dt = DateTime()
dt1 = dt - 3.141592653
dt2 = DateTime(
......@@ -190,11 +193,11 @@ class DateTimeTests(unittest.TestCase):
self.failUnless(not (dt == dt1))
def testUpgradeOldInstances(self):
# Compare dates that don't have the _millis attribute yet
# Compare dates that don't have the _micros attribute yet
dt = DateTime('1997/1/1')
dt1 = DateTime('1997/2/2')
del dt._millis
del dt1._millis
del dt._micros
del dt1._micros
self.testCompareOperations(dt, dt1)
def testTZ2(self):
......@@ -258,17 +261,21 @@ class DateTimeTests(unittest.TestCase):
def testISO8601(self):
# ISO8601 reference dates
ref0 = DateTime('2002/5/2 8:00am GMT')
ref0 = DateTime('2002/5/2 8:00am')
ref1 = DateTime('2002/5/2 8:00am US/Eastern')
ref2 = DateTime('2006/11/6 10:30 UTC')
ref3 = DateTime('2004/06/14 14:30:15 GMT-3')
ref4 = DateTime('2006/01/01 UTC')
ref5 = DateTime('2002/5/2 8:00am GMT')
# Basic tests
# this is timezone naive and should be interpreted in the local timezone
isoDt = DateTime('2002-05-02T08:00:00')
self.assertEqual(ref0, isoDt)
isoDt = DateTime('2002-05-02T08:00:00Z')
self.assertEqual(ref0, isoDt)
self.assertEqual(ref5, isoDt)
isoDt = DateTime('2002-05-02T08:00:00+00:00')
self.assertEqual(ref5, isoDt)
isoDt = DateTime('2002-05-02T08:00:00-04:00')
self.assertEqual(ref1, isoDt)
isoDt = DateTime('2002-05-02 08:00:00-04:00')
......@@ -302,7 +309,7 @@ class DateTimeTests(unittest.TestCase):
# Bug 2191: timezones with only one digit for hour
isoDt = DateTime('20020502T080000+0')
self.assertEqual(ref0, isoDt)
self.assertEqual(ref5, isoDt)
isoDt = DateTime('20020502 080000-4')
self.assertEqual(ref1, isoDt)
isoDt = DateTime('20020502T080000-400')
......@@ -460,6 +467,45 @@ class DateTimeTests(unittest.TestCase):
ok = dt.strftime('Le %d/%m/%Y a %Hh%M').replace('a', u'\xe0')
self.assertEqual(dt.strftime(u'Le %d/%m/%Y \xe0 %Hh%M'), ok)
def testTimezoneNaiveHandling(self):
# checks that we assign timezone naivity correctly
dt = DateTime('2007-10-04T08:00:00+00:00')
assert dt.timezoneNaive() is False, 'error with naivity handling in __parse_iso8601'
dt = DateTime('2007-10-04T08:00:00Z')
assert dt.timezoneNaive() is False, 'error with naivity handling in __parse_iso8601'
dt = DateTime('2007-10-04T08:00:00')
assert dt.timezoneNaive() is True, 'error with naivity handling in __parse_iso8601'
dt = DateTime('2007/10/04 15:12:33.487618 GMT+1')
assert dt.timezoneNaive() is False, 'error with naivity handling in _parse'
dt = DateTime('2007/10/04 15:12:33.487618')
assert dt.timezoneNaive() is True, 'error with naivity handling in _parse'
dt = DateTime()
assert dt.timezoneNaive() is False, 'error with naivity for current time'
s = '2007-10-04T08:00:00'
dt = DateTime(s)
self.assertEqual(s, dt.ISO8601())
s = '2007-10-04T08:00:00+00:00'
dt = DateTime(s)
self.assertEqual(s, dt.ISO8601())
def testConversions(self):
sdt0 = datetime.now() # this is a timezone naive datetime
dt0 = DateTime(sdt0)
assert dt0.timezoneNaive() is True, (sdt0, dt0)
sdt1 = datetime(2007, 10, 4, 18, 14, 42, 580, pytz.utc)
dt1 = DateTime(sdt1)
assert dt1.timezoneNaive() is False, (sdt1, dt1)
# convert back
sdt2 = dt0.asdatetime()
self.assertEqual(sdt0, sdt2)
sdt3 = dt1.utcdatetime() # this returns a timezone naive datetime
self.assertEqual(sdt1.hour, sdt3.hour)
dt4 = DateTime('2007-10-04T10:00:00+05:00')
sdt4 = datetime(2007, 10, 4, 5, 0)
self.assertEqual(dt4.utcdatetime(), sdt4)
def test_suite():
from zope.testing import doctest
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment