Skip to content

Commit

Permalink
Merge 39fe1a1 into 3c18ced
Browse files Browse the repository at this point in the history
  • Loading branch information
jswhit committed Aug 29, 2020
2 parents 3c18ced + 39fe1a1 commit 1f49766
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 51 deletions.
9 changes: 7 additions & 2 deletions Changelog
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
since version 1.2.1
===================
version 1.2.2 (not yet released)
================================
* zero pad years in strtime (issue #194)
* have `cftime.datetime` create calendar-specific instances that support
addition and subtraction (issue #198). Name of base class changed
from `cftime.datetime` to `cftime.datetime_base` (if your are using
`isinstance(my_datetime, cftime.datetime)` this will need to be changed
to `isinstance(my_datetime, cftime.datetime_base)`.

version 1.2.1 (release tag v1.2.1rel)
=====================================
Expand Down
2 changes: 1 addition & 1 deletion cftime/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from ._cftime import utime, JulianDayFromDate, DateFromJulianDay, UNIT_CONVERSION_FACTORS
from ._cftime import _parse_date, date2index, time2index
from ._cftime import datetime, real_datetime
from ._cftime import datetime, real_datetime, datetime_base
from ._cftime import DatetimeNoLeap, DatetimeAllLeap, Datetime360Day, DatetimeJulian, \
DatetimeGregorian, DatetimeProlepticGregorian
from ._cftime import microsec_units, millisec_units, \
Expand Down
105 changes: 59 additions & 46 deletions cftime/_cftime.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ cdef int32_t* days_per_month_array = [
_rop_lookup = {Py_LT: '__gt__', Py_LE: '__ge__', Py_EQ: '__eq__',
Py_GT: '__lt__', Py_GE: '__le__', Py_NE: '__ne__'}

__version__ = '1.2.1'
__version__ = '1.2.2'

# Adapted from http://delete.me.uk/2005/03/iso8601.html
# Note: This regex ensures that all ISO8601 timezone formats are accepted - but, due to legacy support for other timestrings, not all incorrect formats can be rejected.
Expand Down Expand Up @@ -148,8 +148,8 @@ def _dateparse(timestr,calendar):
pass
if not basedate:
if not utc_offset:
basedate = datetime(year, month, day, hour, minute, second,
microsecond)
basedate = datetime_base(year, month, day, hour, minute, second,
microsecond)
else:
raise ValueError('cannot use utc_offset for this reference date/calendar')
if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
Expand Down Expand Up @@ -246,7 +246,7 @@ def date2num(dates,units,calendar='standard'):
# remove time zone offset
if getattr(date, 'tzinfo',None) is not None:
date = date.replace(tzinfo=None) - date.utcoffset()
else: # convert date to same calendar specific cftime.datetime instance
else: # convert date to same calendar specific cftime.datetime_base instance
if not isinstance(date, DATE_TYPES[calendar]):
date = to_calendar_specific_datetime(date, calendar, False)
if ismasked and mask.flat[n]:
Expand Down Expand Up @@ -332,20 +332,20 @@ DATE_TYPES = {
}


def to_calendar_specific_datetime(datetime, calendar, use_python_datetime):
def to_calendar_specific_datetime(dt, calendar, use_python_datetime):
if use_python_datetime:
date_type = real_datetime
else:
date_type = DATE_TYPES[calendar]

return date_type(
datetime.year,
datetime.month,
datetime.day,
datetime.hour,
datetime.minute,
datetime.second,
datetime.microsecond
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second,
dt.microsecond
)


Expand Down Expand Up @@ -435,7 +435,7 @@ def num2date(
**only_use_cftime_datetimes**: if False, python datetime.datetime
objects are returned from num2date where possible; if True dates which
subclass cftime.datetime are returned for all calendars. Default **True**.
subclass cftime.datetime_base are returned for all calendars. Default **True**.
**only_use_python_datetimes**: always return python datetime.datetime
objects and raise an error if this is not possible. Ignored unless
Expand Down Expand Up @@ -498,7 +498,7 @@ def num2date(

# Through np.timedelta64, convert integers scaled to have units of
# microseconds to datetime.timedelta objects, the timedelta type compatible
# with all cftime.datetime objects.
# with all cftime.datetime_base objects.
deltas = scaled_times.astype("timedelta64[us]").astype(timedelta)
try:
return basedate + deltas
Expand Down Expand Up @@ -841,11 +841,24 @@ cdef to_tuple(dt):
dt.second, dt.microsecond)

@cython.embedsignature(True)
cdef class datetime(object):
def datetime(year, month, day, hour=0, minute=0,
second=0, microsecond=0, dayofwk=-1,
dayofyr = -1, calendar='standard'):
"""
Create a calendar specific datetime instance.
"""
if calendar == '':
return datetime_base(year,month,day,hour,minute,second,microsecond,dayofwk,dayofyr,calendar=calendar)
else:
date_type = DATE_TYPES[calendar]
return date_type(year,month,day,hour,minute,second,microsecond,dayofwk,dayofyr)

cdef class datetime_base(object):
"""
The base class implementing most methods of datetime classes that
mimic datetime.datetime but support calendars other than the proleptic
Gregorial calendar.
Gregorial calendar. The factory function cftime.datetime should be used
to create calendar-specific sub-class instances.
"""
cdef readonly int year, month, day, hour, minute
cdef readonly int second, microsecond
Expand All @@ -854,7 +867,7 @@ Gregorial calendar.

# Python's datetime.datetime uses the proleptic Gregorian
# calendar. This boolean is used to decide whether a
# cftime.datetime instance can be converted to
# cftime.datetime_base instance can be converted to
# datetime.datetime.
cdef readonly bint datetime_compatible

Expand Down Expand Up @@ -997,9 +1010,9 @@ Gregorial calendar.
self.second, self.microsecond)

def __richcmp__(self, other, int op):
cdef datetime dt, dt_other
cdef datetime_base dt, dt_other
dt = self
if isinstance(other, datetime):
if isinstance(other, datetime_base):
dt_other = other
# comparing two datetime instances
if dt.calendar == dt_other.calendar:
Expand Down Expand Up @@ -1054,22 +1067,22 @@ Gregorial calendar.
return NotImplemented

def __add__(self, other):
cdef datetime dt
if isinstance(self, datetime) and isinstance(other, timedelta):
cdef datetime_base dt
if isinstance(self, datetime_base) and isinstance(other, timedelta):
dt = self
delta = other
elif isinstance(self, timedelta) and isinstance(other, datetime):
elif isinstance(self, timedelta) and isinstance(other, datetime_base):
dt = other
delta = self
else:
return NotImplemented
return dt._add_timedelta(delta)

def __sub__(self, other):
cdef datetime dt
if isinstance(self, datetime): # left arg is a datetime instance
cdef datetime_base dt
if isinstance(self, datetime_base): # left arg is a datetime instance
dt = self
if isinstance(other, datetime):
if isinstance(other, datetime_base):
# datetime - datetime
if dt.calendar != other.calendar:
raise ValueError("cannot compute the time difference between dates with different calendars")
Expand Down Expand Up @@ -1113,13 +1126,13 @@ datetime object."""
return NotImplemented

@cython.embedsignature(True)
cdef class DatetimeNoLeap(datetime):
cdef class DatetimeNoLeap(datetime_base):
"""
Phony datetime object which mimics the python datetime object,
but uses the "noleap" ("365_day") calendar.
"""
def __init__(self, *args, **kwargs):
datetime.__init__(self, *args, **kwargs)
datetime_base.__init__(self, *args, **kwargs)
self.calendar = "noleap"
self.datetime_compatible = False
assert_valid_date(self, no_leap, False, has_year_zero=True)
Expand All @@ -1132,13 +1145,13 @@ but uses the "noleap" ("365_day") calendar.
return _dpm[self.month-1]

@cython.embedsignature(True)
cdef class DatetimeAllLeap(datetime):
cdef class DatetimeAllLeap(datetime_base):
"""
Phony datetime object which mimics the python datetime object,
but uses the "all_leap" ("366_day") calendar.
"""
def __init__(self, *args, **kwargs):
datetime.__init__(self, *args, **kwargs)
datetime_base.__init__(self, *args, **kwargs)
self.calendar = "all_leap"
self.datetime_compatible = False
assert_valid_date(self, all_leap, False, has_year_zero=True)
Expand All @@ -1151,13 +1164,13 @@ but uses the "all_leap" ("366_day") calendar.
return _dpm_leap[self.month-1]

@cython.embedsignature(True)
cdef class Datetime360Day(datetime):
cdef class Datetime360Day(datetime_base):
"""
Phony datetime object which mimics the python datetime object,
but uses the "360_day" calendar.
"""
def __init__(self, *args, **kwargs):
datetime.__init__(self, *args, **kwargs)
datetime_base.__init__(self, *args, **kwargs)
self.calendar = "360_day"
self.datetime_compatible = False
assert_valid_date(self, no_leap, False, has_year_zero=True, is_360_day=True)
Expand All @@ -1170,13 +1183,13 @@ but uses the "360_day" calendar.
return _dpm_360[self.month-1]

@cython.embedsignature(True)
cdef class DatetimeJulian(datetime):
cdef class DatetimeJulian(datetime_base):
"""
Phony datetime object which mimics the python datetime object,
but uses the "julian" calendar.
"""
def __init__(self, *args, **kwargs):
datetime.__init__(self, *args, **kwargs)
datetime_base.__init__(self, *args, **kwargs)
self.calendar = "julian"
self.datetime_compatible = False
assert_valid_date(self, is_leap_julian, False)
Expand All @@ -1185,7 +1198,7 @@ but uses the "julian" calendar.
return DatetimeJulian(*add_timedelta(self, delta, is_leap_julian, False, False))

@cython.embedsignature(True)
cdef class DatetimeGregorian(datetime):
cdef class DatetimeGregorian(datetime_base):
"""
Phony datetime object which mimics the python datetime object,
but uses the mixed Julian-Gregorian ("standard", "gregorian") calendar.
Expand All @@ -1199,7 +1212,7 @@ datetime.datetime instances and used to compute time differences
a datetime.datetime instance or vice versa.
"""
def __init__(self, *args, **kwargs):
datetime.__init__(self, *args, **kwargs)
datetime_base.__init__(self, *args, **kwargs)
self.calendar = "gregorian"

# dates after 1582-10-15 can be converted to and compared to
Expand All @@ -1214,7 +1227,7 @@ a datetime.datetime instance or vice versa.
return DatetimeGregorian(*add_timedelta(self, delta, is_leap_gregorian, True, False))

@cython.embedsignature(True)
cdef class DatetimeProlepticGregorian(datetime):
cdef class DatetimeProlepticGregorian(datetime_base):
"""
Phony datetime object which mimics the python datetime object,
but allows for dates that don't exist in the proleptic gregorian calendar.
Expand All @@ -1225,14 +1238,14 @@ Has strftime, timetuple, replace, __repr__, and __str__ methods. The
format of the string produced by __str__ is controlled by self.format
(default %Y-%m-%d %H:%M:%S). Supports comparisons with other
datetime instances using the same calendar; comparison with
native python datetime instances is possible for cftime.datetime
native python datetime instances is possible for cftime.datetime_base
instances using 'gregorian' and 'proleptic_gregorian' calendars.
Instance variables are year,month,day,hour,minute,second,microsecond,dayofwk,dayofyr,
format, and calendar.
"""
def __init__(self, *args, **kwargs):
datetime.__init__(self, *args, **kwargs)
datetime_base.__init__(self, *args, **kwargs)
self.calendar = "proleptic_gregorian"
self.datetime_compatible = True
assert_valid_date(self, is_leap_proleptic_gregorian, False)
Expand Down Expand Up @@ -1261,7 +1274,7 @@ cdef _findall(text, substr):
# calendar. ;)


cdef _strftime(datetime dt, fmt):
cdef _strftime(datetime_base dt, fmt):
if _illegal_s.search(fmt):
raise TypeError("This strftime implementation does not handle %s")
# don't use strftime method at all.
Expand Down Expand Up @@ -1321,7 +1334,7 @@ cdef int * month_lengths(bint (*is_leap)(int), int year):
else:
return _dpm

cdef void assert_valid_date(datetime dt, bint (*is_leap)(int),
cdef void assert_valid_date(datetime_base dt, bint (*is_leap)(int),
bint julian_gregorian_mixed,
bint has_year_zero=False,
bint is_360_day=False) except *:
Expand Down Expand Up @@ -1356,7 +1369,7 @@ cdef void assert_valid_date(datetime dt, bint (*is_leap)(int),
if dt.microsecond < 0 or dt.microsecond > 999999:
raise ValueError("invalid microsecond provided in {0!r}".format(dt))

# Add a datetime.timedelta to a cftime.datetime instance. Uses
# Add a datetime.timedelta to a cftime.datetime_base instance. Uses
# integer arithmetic to avoid rounding errors and preserve
# microsecond accuracy.
#
Expand All @@ -1370,7 +1383,7 @@ cdef void assert_valid_date(datetime dt, bint (*is_leap)(int),
# The date of the transition from the Julian to Gregorian calendar and
# the number of invalid dates are hard-wired (1582-10-4 is the last day
# of the Julian calendar, after which follows 1582-10-15).
cdef tuple add_timedelta(datetime dt, delta, bint (*is_leap)(int), bint julian_gregorian_mixed, bint has_year_zero):
cdef tuple add_timedelta(datetime_base dt, delta, bint (*is_leap)(int), bint julian_gregorian_mixed, bint has_year_zero):
cdef int microsecond, second, minute, hour, day, month, year
cdef int delta_microseconds, delta_seconds, delta_days
cdef int* month_length
Expand Down Expand Up @@ -1444,13 +1457,13 @@ cdef tuple add_timedelta(datetime dt, delta, bint (*is_leap)(int), bint julian_g

return (year, month, day, hour, minute, second, microsecond, -1, -1)

# Add a datetime.timedelta to a cftime.datetime instance with the 360_day calendar.
# Add a datetime.timedelta to a cftime.datetime_base instance with the 360_day calendar.
#
# Assumes that the 360_day,365_day and 366_day calendars (unlike the rest of supported
# calendars) have the year 0. Also, there are no leap years and all
# months are 30 days long, so we can compute month and year by using
# "//" and "%".
cdef tuple add_timedelta_360_day(datetime dt, delta):
cdef tuple add_timedelta_360_day(datetime_base dt, delta):
cdef int microsecond, second, minute, hour, day, month, year
cdef int delta_microseconds, delta_seconds, delta_days

Expand Down Expand Up @@ -1870,7 +1883,7 @@ def DateFromJulianDay(JD, calendar='standard', only_use_cftime_datetimes=True,
if calendar='julian', Julian Day follows julian calendar.
If only_use_cftime_datetimes is set to True, then cftime.datetime
If only_use_cftime_datetimes is set to True, then cftime.datetime_base
objects are returned for all calendars. Otherwise the datetime object is a
native python datetime object if the date falls in the Gregorian calendar
(i.e. calendar='proleptic_gregorian', or calendar = 'standard'/'gregorian'
Expand Down Expand Up @@ -2121,7 +2134,7 @@ are:
@keyword only_use_cftime_datetimes: if False, datetime.datetime
objects are returned from num2date where possible; if True dates which subclass
cftime.datetime are returned for all calendars. Default True.
cftime.datetime_base are returned for all calendars. Default True.
@keyword only_use_python_datetimes: always return python datetime.datetime
objects and raise an error if this is not possible. Ignored unless
Expand Down
4 changes: 2 additions & 2 deletions test/test_cftime.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def test_tz_naive(self):
# check date2num,num2date methods.
# use datetime from cftime, since this date doesn't
# exist in "normal" calendars.
d = datetimex(2000, 2, 30)
d = datetimex(2000, 2, 30, calendar='360_day')
t1 = self.cdftime_360day.date2num(d)
assert_almost_equal(t1, 360 * 400.)
d2 = self.cdftime_360day.num2date(t1)
Expand Down Expand Up @@ -1581,7 +1581,7 @@ def test_num2date_only_use_cftime_datetimes_post_gregorian(

def test_repr():
#expected = 'cftime.datetime(2000-01-01 00:00:00)'
expected = 'cftime.datetime(2000, 1, 1, 0, 0, 0, 0)'
expected = 'cftime.DatetimeGregorian(2000, 1, 1, 0, 0, 0, 0)'
assert repr(datetimex(2000, 1, 1)) == expected


Expand Down

0 comments on commit 1f49766

Please sign in to comment.