Skip to content

Commit

Permalink
AbstractDateTime refactoring
Browse files Browse the repository at this point in the history
  - Completed fromdelta and todelta methods
  - common_era_delta property renamed to todelta()
  - Update tests for new code
  - Update CHANGELOG.rst
  • Loading branch information
brunato committed Jan 19, 2019
1 parent 89a1f6e commit c505c43
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 84 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
CHANGELOG
*********

`v1.1.1`_ (2018-01-16)
`v1.1.1`_ (2018-01-19)
======================
* Improvements and fixes for XSD datatypes
* Rewritten AbstractDateTime for supporting years with value > 9999
Expand Down
95 changes: 72 additions & 23 deletions elementpath/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,24 @@ def adjust_day(year, month, day):
return min(day, 29) if isleap(year) else min(day, 28)


def leapdays_from_common_era(days):
if -306 <= days <= 1520:
return 0
days = days - 1520 if days > 0 else -days - 306
return 1 + days // 1461 - days // 36524 + days // 146097
def days_from_common_era(year):
"""
Returns the number of days from from 0001-01-01 to the provided year. For a
common era year the days are counted until the last day of December, for a
BCE year the days are counted down from the end to the 1st of January.
"""
if year > 0:
return year * 365 + year // 4 - year // 100 + year // 400
elif year >= -1:
return year * 366
else:
year = -year - 1
return -(366 + year * 365 + year // 4 - year // 100 + year // 400)


DAYS_IN_4Y = days_from_common_era(4)
DAYS_IN_100Y = days_from_common_era(100)
DAYS_IN_400Y = days_from_common_era(400)


def months2days(year, month, months_delta):
Expand Down Expand Up @@ -343,32 +356,59 @@ def __str__(self):
pass

@classmethod
def fromdelta(cls, delta):
def fromdelta(cls, delta, adjust_timezone=False):
"""
Creates an XSD dateTime/date instance from a datetime.timedelta related to
0001-01-01T00:00:00 CE. In case of a date the time part is not counted.
:param delta: a datetime.timedelta instance.
:param adjust_timezone: if `True` adjust the timezone of Date objects \
with eventually present hours and minutes.
"""
try:
dt = datetime.datetime(1, 1, 1) + delta
except OverflowError:
if delta.total_seconds() > 0:
leap_days = leapdays_from_common_era(delta.days)
year = 1 + (delta.days - leap_days) // 365
days = delta.days - (year - 1) * 365 - leap_days
td = datetime.timedelta(days=days, seconds=delta.seconds, microseconds=delta.microseconds)
dt = datetime.datetime(year, 1, 1) + td
else:
leap_days = leapdays_from_common_era(delta.days)
year = (delta.days + leap_days) // 365
days = delta.days - year * 365 + leap_days
days = delta.days
if days > 0:
y400, days = divmod(days, DAYS_IN_400Y)
y100, days = divmod(days, DAYS_IN_100Y)
y4, days = divmod(days, DAYS_IN_4Y)
y1, days = divmod(days, 365)
year = y400 * 400 + y100 * 100 + y4 * 4 + y1 + 1
if y1 == 4 or y100 == 4:
year -= 1
days = 365

td = datetime.timedelta(days=days, seconds=delta.seconds, microseconds=delta.microseconds)
dt = datetime.datetime(4 if isleap(year) else 6, 1, 1) + td

elif days >= -366:
year = -1
td = datetime.timedelta(days=days, seconds=delta.seconds, microseconds=delta.microseconds)
dt = datetime.datetime(5, 1, 1) + td

else:
days = -days - 366
y400, days = divmod(days, DAYS_IN_400Y)
y100, days = divmod(days, DAYS_IN_100Y)
y4, days = divmod(days, DAYS_IN_4Y)
y1, days = divmod(days, 365)
year = -y400 * 400 - y100 * 100 - y4 * 4 - y1 - 2
if y1 == 4 or y100 == 4:
year += 1
days = 365

td = datetime.timedelta(days=-days, seconds=delta.seconds, microseconds=delta.microseconds)
if not td:
dt = datetime.datetime(4 if isleap(year + 1) else 6, 1, 1)
year += 1
else:
dt = datetime.datetime(5 if isleap(year + 1) else 7, 1, 1) + td
else:
year = dt.year

if issubclass(cls, Date):
# Adjust timezone info for dates
if dt.hour or dt.minute:
if adjust_timezone and dt.hour or dt.minute:
if dt.tzinfo is None:
hour, minute = dt.hour, dt.minute
else:
Expand All @@ -384,12 +424,21 @@ def fromdelta(cls, delta):
return cls(year, dt.month, dt.day, tzinfo=dt.tzinfo)
return cls(year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo)

@property
def common_era_delta(self):
"""Property that returns the datetime.timedelta from 0001-01-01T00:00:00 CE."""
def todelta(self):
"""Returns the datetime.timedelta from 0001-01-01T00:00:00 CE."""
if self._year is None:
return self._dt - datetime.datetime(1, 1, 1)

year, dt = self.year, self._dt
tzinfo = None if dt.tzinfo is None else self._utc_timezone
days = months2days(1, 1, (self.year - 1 if year > 0 else year) * 12 + dt.month - 1)

if year > 0:
m_days = MONTH_DAYS_LEAP if isleap(year) else MONTH_DAYS
days = days_from_common_era(year - 1) + sum(m_days[m] for m in range(1, dt.month))
else:
m_days = MONTH_DAYS_LEAP if isleap(year + 1) else MONTH_DAYS
days = days_from_common_era(year) + sum(m_days[m] for m in range(1, dt.month))

delta = (dt - datetime.datetime(dt.year, dt.month, day=1, tzinfo=tzinfo))
return datetime.timedelta(days=days, seconds=delta.total_seconds())

Expand All @@ -398,7 +447,7 @@ def _date_operator(self, op, other):
dt1, dt2 = self._get_operands(other)
if self._year is None and other._year is None:
return DayTimeDuration.fromtimedelta(dt1 - dt2)
return DayTimeDuration.fromtimedelta(self.common_era_delta - other.common_era_delta)
return DayTimeDuration.fromtimedelta(self.todelta() - other.todelta())

elif isinstance(other, (DayTimeDuration, datetime.timedelta)):
delta = other.get_timedelta() if isinstance(other, DayTimeDuration) else other
Expand Down
152 changes: 92 additions & 60 deletions tests/test_datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
import random
from decimal import Decimal
from calendar import isleap
from elementpath.datatypes import months2days, DateTime, DateTime10, Date, Date10, Time, Timezone, \
Duration, DayTimeDuration, YearMonthDuration, UntypedAtomic, GregorianYear, GregorianYear10, \
GregorianYearMonth, GregorianYearMonth10, GregorianMonthDay, GregorianMonth, GregorianDay, \
AbstractDateTime, OrderedDateTime
from elementpath.datatypes import MONTH_DAYS, MONTH_DAYS_LEAP, days_from_common_era, months2days, \
DateTime, DateTime10, Date, Date10, Time, Timezone, Duration, DayTimeDuration, YearMonthDuration, \
UntypedAtomic, GregorianYear, GregorianYear10, GregorianYearMonth, GregorianYearMonth10, \
GregorianMonthDay, GregorianMonth, GregorianDay, AbstractDateTime, OrderedDateTime


class UntypedAtomicTest(unittest.TestCase):
Expand Down Expand Up @@ -279,6 +279,31 @@ def test_ge_operator(self):
self.assertTrue(mkdt("15032-11-12T23:17:59Z") >= mkdt("15032-11-12T23:17:59Z"))
self.assertRaises(TypeError, operator.le, mkdt("2002-04-02T18:00:00+02:00"), mkdate("2002-04-03"))

def test_days_from_common_era_function(self):
days4y = 365 * 3 + 366
days100y = days4y * 24 + 365 * 4
days400y = days100y * 4 + 1

self.assertEqual(days_from_common_era(0), 0)
self.assertEqual(days_from_common_era(1), 365)
self.assertEqual(days_from_common_era(3), 365 * 3)
self.assertEqual(days_from_common_era(4), days4y)
self.assertEqual(days_from_common_era(100), days100y)
self.assertEqual(days_from_common_era(200), days100y * 2)
self.assertEqual(days_from_common_era(300), days100y * 3)
self.assertEqual(days_from_common_era(400), days400y)
self.assertEqual(days_from_common_era(800), 2 * days400y)
self.assertEqual(days_from_common_era(-1), -366)
self.assertEqual(days_from_common_era(-4), -days4y)
self.assertEqual(days_from_common_era(-5), -days4y - 366)
self.assertEqual(days_from_common_era(-100), -days100y - 1)
self.assertEqual(days_from_common_era(-200), -days100y * 2 - 1)
self.assertEqual(days_from_common_era(-300), -days100y * 3 - 1)
self.assertEqual(days_from_common_era(-101), -days100y - 366)
self.assertEqual(days_from_common_era(-400), -days400y)
self.assertEqual(days_from_common_era(-401), -days400y - 366)
self.assertEqual(days_from_common_era(-800), -days400y * 2)

def test_months2days_function(self):
self.assertEqual(months2days(-119, 1, 12 * 319), 116512)
self.assertEqual(months2days(200, 1, -12 * 320) - 1, -116877 - 2)
Expand All @@ -289,31 +314,6 @@ def test_months2days_function(self):
self.assertEqual(months2days(1, 1, 12), 365)
self.assertEqual(months2days(1, 1, -12), -366)

def test_common_era_delta(self):
self.assertEqual(Date.fromstring("0001-01-01").common_era_delta, datetime.timedelta(days=0))
self.assertEqual(Date.fromstring("0001-02-01").common_era_delta, datetime.timedelta(days=31))
self.assertEqual(Date.fromstring("0001-03-01").common_era_delta, datetime.timedelta(days=59))
self.assertEqual(Date.fromstring("0001-06-01").common_era_delta, datetime.timedelta(days=151))
self.assertEqual(Date.fromstring("0001-06-03").common_era_delta, datetime.timedelta(days=153))
self.assertEqual(DateTime.fromstring("0001-06-03T20:00:00").common_era_delta,
datetime.timedelta(days=153, seconds=72000))

self.assertEqual(Date.fromstring("0002-01-01").common_era_delta, datetime.timedelta(days=365))
self.assertEqual(Date.fromstring("0002-02-01").common_era_delta, datetime.timedelta(days=396))

self.assertEqual(Date.fromstring("-0000-01-01").common_era_delta, datetime.timedelta(days=-366))
self.assertEqual(Date.fromstring("-0000-02-01").common_era_delta, datetime.timedelta(days=-335))
self.assertEqual(Date.fromstring("-0000-12-31").common_era_delta, datetime.timedelta(days=-1))

self.assertEqual(Date10.fromstring("-0001-01-01").common_era_delta, datetime.timedelta(days=-366))
self.assertEqual(Date10.fromstring("-0001-02-10").common_era_delta, datetime.timedelta(days=-326))
self.assertEqual(Date10.fromstring("-0001-12-31Z").common_era_delta, datetime.timedelta(days=-1))
self.assertEqual(Date10.fromstring("-0001-12-31-02:00").common_era_delta, datetime.timedelta(hours=-22))
self.assertEqual(Date10.fromstring("-0001-12-31+03:00").common_era_delta, datetime.timedelta(hours=-27))
self.assertEqual(Date10.fromstring("-0001-12-31+03:00").common_era_delta, datetime.timedelta(hours=-27))
self.assertEqual(Date10.fromstring("-0001-12-31+03:12").common_era_delta,
datetime.timedelta(hours=-27, minutes=-12))

def test_fromdelta(self):
self.assertIsNotNone(Date.fromstring('10000-02-28'))
self.assertEqual(Date.fromdelta(datetime.timedelta(days=0)), Date.fromstring("0001-01-01"))
Expand All @@ -335,42 +335,74 @@ def test_fromdelta(self):
self.assertEqual(Date10.fromdelta(datetime.timedelta(days=-366)), Date10.fromstring("-0001-01-01"))
self.assertEqual(Date10.fromdelta(datetime.timedelta(days=-326)), Date10.fromstring("-0001-02-10"))
self.assertEqual(Date10.fromdelta(datetime.timedelta(days=-1)), Date10.fromstring("-0001-12-31Z"))
self.assertEqual(Date10.fromdelta(datetime.timedelta(hours=-22)), Date10.fromstring("-0001-12-31-02:00"))
self.assertEqual(Date10.fromdelta(datetime.timedelta(hours=-27)), Date10.fromstring("-0001-12-31+03:00"))
self.assertEqual(Date10.fromdelta(datetime.timedelta(hours=-27, minutes=-12)),

# With timezone adjusting
self.assertEqual(Date10.fromdelta(datetime.timedelta(hours=-22), adjust_timezone=True),
Date10.fromstring("-0001-12-31-02:00"))
self.assertEqual(Date10.fromdelta(datetime.timedelta(hours=-27), adjust_timezone=True),
Date10.fromstring("-0001-12-31+03:00"))
self.assertEqual(Date10.fromdelta(datetime.timedelta(hours=-27, minutes=-12), adjust_timezone=True),
Date10.fromstring("-0001-12-31+03:12"))

self.assertEqual(DateTime10.fromdelta(datetime.timedelta(hours=-27, minutes=-12, seconds=-5)),
DateTime10.fromstring("-0001-12-30T20:47:55"))

def test_common_era_delta_with_fromdelta(self):
days = 68 # 0001-03-10
for year in range(1, 15000):
if year <= 100 or 9900 <= year <= 10100 or random.randint(1, 20) == 1:
date_string = '{:04}-03-10'.format(year) if year < 10000 else '{}-03-10'.format(year)
dt1 = Date10.fromstring(date_string)
delta1 = dt1.common_era_delta
delta2 = datetime.timedelta(days=days)
self.assertEqual(delta1, delta2, msg="Fails for %r: %r != %r" % (dt1, delta1, delta2))
#dt2 = Date10.fromdelta(delta2)
# if dt1 != dt2:
# import pdb
# pdb.set_trace()
# self.assertEqual(dt1, dt2)
days += 366 if isleap(year + 1) else 365
return

days = -220 # -0001-05-26
for year in range(-1, -15000, -1):
if year >= -100 or -9900 >= year >= -10100 or random.randint(1, 20) == 1:
date_string = '-{:04}-05-26'.format(abs(year)) if year > -10000 else '{}-05-26'.format(year)
dt1 = Date10.fromstring(date_string)
delta1 = dt1.common_era_delta
delta2 = datetime.timedelta(days=days)
self.assertEqual(delta1, delta2, msg="Fails for year %r: %r != %r" % (dt1, delta1, delta2))
dt2 = Date10.fromdelta(delta2)
self.assertEqual(dt1, dt2)
days -= 366 if isleap(year + 1) else 365
def test_todelta(self):
self.assertEqual(Date.fromstring("0001-01-01").todelta(), datetime.timedelta(days=0))
self.assertEqual(Date.fromstring("0001-02-01").todelta(), datetime.timedelta(days=31))
self.assertEqual(Date.fromstring("0001-03-01").todelta(), datetime.timedelta(days=59))
self.assertEqual(Date.fromstring("0001-06-01").todelta(), datetime.timedelta(days=151))
self.assertEqual(Date.fromstring("0001-06-03").todelta(), datetime.timedelta(days=153))
self.assertEqual(DateTime.fromstring("0001-06-03T20:00:00").todelta(),
datetime.timedelta(days=153, seconds=72000))

self.assertEqual(Date.fromstring("0002-01-01").todelta(), datetime.timedelta(days=365))
self.assertEqual(Date.fromstring("0002-02-01").todelta(), datetime.timedelta(days=396))

self.assertEqual(Date.fromstring("-0000-01-01").todelta(), datetime.timedelta(days=-366))
self.assertEqual(Date.fromstring("-0000-02-01").todelta(), datetime.timedelta(days=-335))
self.assertEqual(Date.fromstring("-0000-12-31").todelta(), datetime.timedelta(days=-1))

self.assertEqual(Date10.fromstring("-0001-01-01").todelta(), datetime.timedelta(days=-366))
self.assertEqual(Date10.fromstring("-0001-02-10").todelta(), datetime.timedelta(days=-326))
self.assertEqual(Date10.fromstring("-0001-12-31Z").todelta(), datetime.timedelta(days=-1))
self.assertEqual(Date10.fromstring("-0001-12-31-02:00").todelta(), datetime.timedelta(hours=-22))
self.assertEqual(Date10.fromstring("-0001-12-31+03:00").todelta(), datetime.timedelta(hours=-27))
self.assertEqual(Date10.fromstring("-0001-12-31+03:00").todelta(), datetime.timedelta(hours=-27))
self.assertEqual(Date10.fromstring("-0001-12-31+03:12").todelta(),
datetime.timedelta(hours=-27, minutes=-12))

def test_to_and_from_delta(self):
for month, day in [(1, 1), (1, 2), (2, 1), (2, 28), (3, 10), (6, 30), (12, 31)]:
fmt1 = '{:04}-%s' % '{:02}-{:02}'.format(month, day)
fmt2 = '{}-%s' % '{:02}-{:02}'.format(month, day)
days = sum(MONTH_DAYS[m] for m in range(1, month)) + day - 1
for year in range(1, 15000):
if year <= 500 or 9900 <= year <= 10100 or random.randint(1, 20) == 1:
date_string = fmt1.format(year) if year < 10000 else fmt2.format(year)
dt1 = Date10.fromstring(date_string)
delta1 = dt1.todelta()
delta2 = datetime.timedelta(days=days)
self.assertEqual(delta1, delta2, msg="Failed for %r: %r != %r" % (dt1, delta1, delta2))
dt2 = Date10.fromdelta(delta2)
self.assertEqual(dt1, dt2, msg="Failed for year %d: %r != %r" % (year, dt1, dt2))
days += 366 if isleap(year if month <= 2 else year + 1) else 365

def test_to_and_from_delta_bce(self):
for month, day in [(1, 1), (1, 2), (2, 1), (2, 28), (3, 10), (5, 26), (6, 30), (12, 31)]:
fmt1 = '-{:04}-%s' % '{:02}-{:02}'.format(month, day)
fmt2 = '{}-%s' % '{:02}-{:02}'.format(month, day)
days = -sum(MONTH_DAYS_LEAP[m] for m in range(month, 13)) + day - 1
for year in range(-1, -15000, -1):
if year >= -500 or -9900 >= year >= -10100 or random.randint(1, 20) == 1:
date_string = fmt1.format(abs(year)) if year > -10000 else fmt2.format(year)
dt1 = Date10.fromstring(date_string)
delta1 = dt1.todelta()
delta2 = datetime.timedelta(days=days)
self.assertEqual(delta1, delta2, msg="Failed for %r: %r != %r" % (dt1, delta1, delta2))
dt2 = Date10.fromdelta(delta2)
self.assertEqual(dt1, dt2, msg="Failed for year %d: %r != %r" % (year, dt1, dt2))
days -= 366 if isleap(year if month <= 2 else year + 1) else 365

def test_sub_operator(self):
date = Date.fromstring
Expand Down

0 comments on commit c505c43

Please sign in to comment.