[Git][debian-gis-team/cftime][upstream] New upstream version 1.3.0+ds

Bas Couwenberg gitlab at salsa.debian.org
Wed Dec 23 05:06:48 GMT 2020



Bas Couwenberg pushed to branch upstream at Debian GIS Project / cftime


Commits:
c370f74e by Bas Couwenberg at 2020-12-23T05:54:12+01:00
New upstream version 1.3.0+ds
- - - - -


4 changed files:

- Changelog
- README.md
- cftime/_cftime.pyx
- test/test_cftime.py


Changes:

=====================================
Changelog
=====================================
@@ -1,5 +1,20 @@
-version 1.2.1 (not yet released)
-=================================
+version 1.3.0 (release tag v1.3.0rel)
+=====================================
+ * zero pad years in strtime (issue #194)
+ * have cftime.datetime constuctor create 'calendar-aware' instances (default is
+   'standard' calendar, if calendar='' or None the instance is not calendar aware and some
+   methods, like dayofwk, dayofyr, __add__ and __sub__, will not work). Fixes issue #198.
+   The calendar specific sub-classes are now deprecated, but remain for now
+   as stubs that just instantiate the base class and override __repr__.
+ * update regex in _cpdef _parse_date so reference years with more than four
+   digits can be handled. 
+ * Change default calendar in cftime.date2num from 'standard' to None
+   (calendar associated with first input datetime object is used).
+ * add `cftime.datetime.tzinfo=None` for compatibility with python datetime
+   (issue #208).
+
+version 1.2.1 (release tag v1.2.1rel)
+=====================================
  * num2date uses 'proleptic_gregorian' scheme when basedate is post-Gregorian but date is pre-Gregorian
    (issue #182).
  * fix 1.2.0 regression (date2num no longer works with numpy scalar array inputs, issue #185).


=====================================
README.md
=====================================
@@ -12,6 +12,15 @@ Time-handling functionality from netcdf4-python
 ## News
 For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog).
 
+11/16/2020:  Version 1.3.0 released. **API change**: The `cftime.datetime` constructor now creates 
+ 'calendar-aware' instances (default is `'standard'` calendar, if `calendar=''` or `None` the instance
+ is not calendar aware and some methods, like `dayofwk`, `dayofyr`, `__add__` and `__sub__`, will not work)
+ See discussion for issue [#198](https://github.com/Unidata/cftime/issues/198).
+ The calendar specific sub-classes are now deprecated, but remain for now
+ as stubs that just instantiate the base class and override `__repr__`.
+ The default calendar in `cftime.date2num` has been changed from `'standard'` to `None`
+ (the calendar associated with first input datetime object is used to define the calendar).
+
 07/20/2020: Version 1.2.1 released.  Fixes a couple of regressions introduced in 1.2.0. See Changelog for details.
 
 7/06/2020:  version 1.2.0 released. New microsecond accurate algorithm for date2num/num2date contributed by [spencerkclark](https://github.com/spencerkclark). Bugs fixed in masked array handling.


=====================================
cftime/_cftime.pyx
=====================================
@@ -53,12 +53,12 @@ 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.3.0'
 
 # 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.
 #       For example, the TZ spec "+01:0" will still work even though the minutes value is only one character long.
-ISO8601_REGEX = re.compile(r"(?P<year>[+-]?[0-9]{1,4})(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})"
+ISO8601_REGEX = re.compile(r"(?P<year>[+-]?[0-9]+)(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})"
                            r"(((?P<separator1>.)(?P<hour>[0-9]{1,2}):(?P<minute>[0-9]{1,2})(:(?P<second>[0-9]{1,2})(\.(?P<fraction>[0-9]+))?)?)?"
                            r"((?P<separator2>.?)(?P<timezone>Z|(([-+])([0-9]{2})((:([0-9]{2}))|([0-9]{2}))?)))?)?)?)?"
                            )
@@ -104,9 +104,6 @@ class real_datetime(datetime_python):
         return get_days_in_month(_is_leap(self.year,'proleptic_gregorian'), self.month)
     nanosecond = 0 # workaround for pandas bug (cftime issue #77)
 
-# start of the gregorian calendar
-gregorian = real_datetime(1582,10,15)
-
 def _datesplit(timestr):
     """split a time string into two components, units and the remainder
     after 'since'
@@ -137,39 +134,30 @@ def _dateparse(timestr,calendar):
     # parse the date string.
     year, month, day, hour, minute, second, microsecond, utc_offset =\
         _parse_date( isostring.strip() )
-    basedate = None
-    if year >= MINYEAR and year <= MAXYEAR:
-        try:
-            basedate = real_datetime(year, month, day, hour, minute, second,
-                    microsecond)
-            # subtract utc_offset from basedate time instance (which is timezone naive)
-            basedate -= timedelta(days=utc_offset/1440.)
-        except ValueError:
-            pass
-    if not basedate:
-        if not utc_offset:
-            basedate = datetime(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']:
-        if basedate.year == 0:
+        if year == 0:
             msg='zero not allowed as a reference year, does not exist in Julian or Gregorian calendars'
             raise ValueError(msg)
-    if calendar in ['noleap', '365_day'] and basedate.month == 2 and basedate.day == 29:
+    if calendar in ['noleap', '365_day'] and month == 2 and day == 29:
         raise ValueError(
             'cannot specify a leap day as the reference time with the noleap calendar')
-    if calendar == '360_day' and basedate.day > 30:
+    if calendar == '360_day' and day > 30:
         raise ValueError(
             'there are only 30 days in every month with the 360_day calendar')
+    basedate = datetime(year, month, day, hour, minute, second,
+                        microsecond,calendar=calendar)
+    # subtract utc_offset from basedate time instance (which is timezone naive)
+    if utc_offset:
+        basedate -= timedelta(days=utc_offset/1440.)
     return basedate
 
 def _can_use_python_datetime(date,calendar):
+    gregorian = datetime(1582,10,15,calendar=calendar)
     return ((calendar == 'proleptic_gregorian' and date.year >= MINYEAR and date.year <= MAXYEAR) or \
            (calendar in ['gregorian','standard'] and date > gregorian and date.year <= MAXYEAR))
 
 @cython.embedsignature(True)
-def date2num(dates,units,calendar='standard'):
+def date2num(dates,units,calendar=None):
     """
     Return numeric time values given datetime objects. The units
     of the numeric time values are described by the **units** argument
@@ -179,55 +167,82 @@ def date2num(dates,units,calendar='standard'):
     returned numeric values.
 
     **dates**: A datetime object or a sequence of datetime objects.
-    The datetime objects should not include a time-zone offset.
+    The datetime objects should not include a time-zone offset. They
+    can be either native python datetime instances (which use
+    the proleptic gregorian calendar) or cftime.datetime instances.
 
     **units**: a string of the form **<time units> since <reference time>**
     describing the time units. **<time units>** can be days, hours, minutes,
     seconds, milliseconds or microseconds. **<reference time>** is the time
     origin. **months_since** is allowed *only* for the **360_day** calendar.
 
-    **calendar**: describes the calendar used in the time calculations.
+    **calendar**: describes the calendar to be used in the time calculations.
     All the values currently defined in the
     [CF metadata convention](http://cfconventions.org)
     Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
-    Default is **'standard'**, which is a mixed Julian/Gregorian calendar.
+    Default is `None` which means the calendar associated with the rist
+    input datetime instance will be used.
 
     returns a numeric time value, or an array of numeric time values
     with approximately 1 microsecond accuracy.
     """
-    calendar = calendar.lower()
-    basedate = _dateparse(units,calendar=calendar)
-    (unit, isostring) = _datesplit(units)
-    # real-world calendars limited to positive reference years.
-    if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
-        if basedate.year == 0:
-            msg='zero not allowed as a reference year, does not exist in Julian or Gregorian calendars'
-            raise ValueError(msg)
-    if unit not in UNIT_CONVERSION_FACTORS:
-        raise ValueError("Unsupported time units provided, {!r}.".format(unit))
-    if unit in ["months", "month"] and calendar != "360_day":
-        raise ValueError("Units of months only valid for 360_day calendar.")
-    factor = UNIT_CONVERSION_FACTORS[unit]
-    can_use_python_basedatetime = _can_use_python_datetime(basedate,calendar)
 
+    # input a scale or array-like?
     isscalar = False
     try:
         dates[0]
     except:
         isscalar = True
+
+    # masked array input?
     ismasked = False
     if np.ma.isMA(dates) and np.ma.is_masked(dates):
         mask = dates.mask
         ismasked = True
-    dates = np.asanyarray(dates)
-    shape = dates.shape
-    # are all dates python datetime instances?
+
+    # are all input dates 'real' python datetime objects?
+    dates = np.asanyarray(dates) # convert to numpy array
+    shape = dates.shape # save shape of input
     all_python_datetimes = True
     for date in dates.flat:
         if not isinstance(date,datetime_python):
            all_python_datetimes = False
            break
+
+    # if calendar is None or '', use calendar of first input cftime.datetime instances.
+    # if inputs are 'real' python datetime instances, use propleptic gregorian.
+    if not calendar:
+        if all_python_datetimes:
+            calendar = 'proleptic_gregorian'
+        else:
+            if isscalar:
+                d0 = dates.item()
+            else:
+                d0 = dates.flat[0]
+            if isinstance(d0,datetime_python):
+                calendar = 'proleptic_gregorian' 
+            else:
+                try:
+                    calendar = d0.calendar
+                except AttributeError:
+                    raise ValueError('no calendar specified',type(d0))
+
+    calendar = calendar.lower()
+    basedate = _dateparse(units,calendar=calendar)
+    (unit, isostring) = _datesplit(units)
+    # real-world calendars cannot have zero as a reference year.
+    if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
+        if basedate.year == 0:
+            msg='zero not allowed as a reference year, does not exist in Julian or Gregorian calendars'
+            raise ValueError(msg)
+    if unit not in UNIT_CONVERSION_FACTORS:
+        raise ValueError("Unsupported time units provided, {!r}.".format(unit))
+    if unit in ["months", "month"] and calendar != "360_day":
+        raise ValueError("Units of months only valid for 360_day calendar.")
+    factor = UNIT_CONVERSION_FACTORS[unit]
+    can_use_python_basedatetime = _can_use_python_datetime(basedate,calendar)
+
     if can_use_python_basedatetime and all_python_datetimes:
         use_python_datetime = True
         if not isinstance(basedate, datetime_python):
@@ -237,8 +252,7 @@ def date2num(dates,units,calendar='standard'):
     else:
         use_python_datetime = False
         # convert basedate to specified calendar
-        if not isinstance(basedate, DATE_TYPES[calendar]):
-            basedate =  to_calendar_specific_datetime(basedate, calendar, False)
+        basedate =  to_calendar_specific_datetime(basedate, calendar, False)
     times = []; n = 0
     for date in dates.flat:
         # use python datetime if possible.
@@ -247,8 +261,7 @@ def date2num(dates,units,calendar='standard'):
             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
-            if not isinstance(date, DATE_TYPES[calendar]):
-                date = to_calendar_specific_datetime(date, calendar, False)
+            date = to_calendar_specific_datetime(date, calendar, False)
         if ismasked and mask.flat[n]:
             times.append(None)
         else:
@@ -332,20 +345,42 @@ 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:
+#        return real_datetime(
+#               dt.year,
+#               dt.month,
+#               dt.day,
+#               dt.hour,
+#               dt.minute,
+#               dt.second,
+#               dt.microsecond)
+#    else:
+#        return datetime(
+#               dt.year,
+#               dt.month,
+#               dt.day,
+#               dt.hour,
+#               dt.minute,
+#               dt.second,
+#               dt.microsecond,
+#               calendar=calendar)
+# return calendar-specific subclasses for backward compatbility,
+# even though after 1.3.0 this is no longer necessary.
+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
     )
 
 
@@ -412,8 +447,7 @@ def num2date(
     only_use_cftime_datetimes=True,
     only_use_python_datetimes=False
 ):
-    """Decode times exactly with timedelta arithmetic.
-
+    """
     Return datetime objects given numeric time values. The units
     of the numeric time values are described by the **units** argument
     and the **calendar** keyword. The returned datetime objects represent
@@ -692,21 +726,21 @@ def _date2index(dates, nctime, calendar=None, select='exact'):
     The datetime objects should not include a time-zone offset.
 
     **nctime**: A netCDF time variable object. The nctime object must have a
-    C{units} attribute. The entries are assumed to be stored in increasing
+    `units` attribute. The entries are assumed to be stored in increasing
     order.
 
     **calendar**: Describes the calendar used in the time calculation.
-    Valid calendars C{'standard', 'gregorian', 'proleptic_gregorian'
-    'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'}.
-    Default is C{'standard'}, which is a mixed Julian/Gregorian calendar
-    If C{calendar} is None, its value is given by C{nctime.calendar} or
-    C{standard} if no such attribute exists.
-
-    **select**: C{'exact', 'before', 'after', 'nearest'}
-    The index selection method. C{exact} will return the indices perfectly
-    matching the dates given. C{before} and C{after} will return the indices
+    Valid calendars 'standard', 'gregorian', 'proleptic_gregorian'
+    'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
+    Default is 'standard', which is a mixed Julian/Gregorian calendar
+    If `calendar` is None, its value is given by `nctime.calendar` or
+    `standard` if no such attribute exists.
+
+    **select**: 'exact', 'before', 'after', 'nearest'
+    The index selection method. `exact` will return the indices perfectly
+    matching the dates given. `before` and `after` will return the indices
     corresponding to the dates just before or just after the given dates if
-    an exact match cannot be found. C{nearest} will return the indices that
+    an exact match cannot be found. `nearest` will return the indices that
     correspond to the closest dates.
     """
     try:
@@ -725,24 +759,24 @@ def time2index(times, nctime, calendar=None, select='exact'):
     """
     Return indices of a netCDF time variable corresponding to the given times.
 
-    **param** times: A numeric time or a sequence of numeric times.
+    **times**: A numeric time or a sequence of numeric times.
 
     **nctime**: A netCDF time variable object. The nctime object must have a
-    C{units} attribute. The entries are assumed to be stored in increasing
+    `units` attribute. The entries are assumed to be stored in increasing
     order.
 
     **calendar**: Describes the calendar used in the time calculation.
-    Valid calendars C{'standard', 'gregorian', 'proleptic_gregorian'
-    'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'}.
-    Default is C{'standard'}, which is a mixed Julian/Gregorian calendar
-    If C{calendar} is None, its value is given by C{nctime.calendar} or
-    C{standard} if no such attribute exists.
+    Valid calendars 'standard', 'gregorian', 'proleptic_gregorian'
+    'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
+    Default is `standard`, which is a mixed Julian/Gregorian calendar
+    If `calendar` is None, its value is given by `nctime.calendar` or
+    `standard` if no such attribute exists.
 
     **select**: **'exact', 'before', 'after', 'nearest'**
-    The index selection method. C{exact} will return the indices perfectly
-    matching the times given. C{before} and C{after} will return the indices
+    The index selection method. `exact` will return the indices perfectly
+    matching the times given. `before` and `after` will return the indices
     corresponding to the times just before or just after the given times if
-    an exact match cannot be found. C{nearest} will return the indices that
+    an exact match cannot be found. `nearest` will return the indices that
     correspond to the closest times.
     """
     try:
@@ -844,14 +878,42 @@ cdef to_tuple(dt):
 @cython.embedsignature(True)
 cdef class datetime(object):
     """
-The base class implementing most methods of datetime classes that
-mimic datetime.datetime but support calendars other than the proleptic
-Gregorial calendar.
+This class mimics datetime.datetime but support calendars other than the proleptic
+Gregorian calendar.
+
+Supports timedelta operations by overloading +/-, and
+comparisons with other instances using the same calendar.
+
+Comparison with native python datetime instances is possible
+for cftime.datetime instances using
+'gregorian' and 'proleptic_gregorian' calendars.
+
+All the calendars currently defined in the
+[CF metadata convention](http://cfconventions.org) are supported.
+Valid calendars are 'standard', 'gregorian', 'proleptic_gregorian'
+'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
+Default is 'standard', which is a mixed Julian/Gregorian calendar.
+'standard' and 'gregorian' are synonyms, as are 'all_leap'/'366_day'
+and 'noleap'/'365_day'.
+
+If the calendar kwarg is set to a blank string ('') or None (the default is 'standard') the 
+instance will not be calendar-aware and some methods will not work.
+
+Has isoformat, strftime, timetuple, replace, dayofwk, dayofyr, daysinmonth,
+__repr__, __add__, __sub__, __str__ and comparison methods. 
+
+dayofwk, dayofyr, daysinmonth, __add__ and __sub__ only work for calendar-aware
+instances.
+
+The default format of the string produced by strftime is controlled by self.format
+(default %Y-%m-%d %H:%M:%S).
     """
     cdef readonly int year, month, day, hour, minute
     cdef readonly int second, microsecond
     cdef readonly str calendar
     cdef readonly int _dayofwk, _dayofyr
+    cdef readonly bint has_year_zero
+    cdef readonly object tzinfo
 
     # Python's datetime.datetime uses the proleptic Gregorian
     # calendar. This boolean is used to decide whether a
@@ -870,10 +932,54 @@ Gregorial calendar.
         self.minute = minute
         self.second = second
         self.microsecond = microsecond
-        self.calendar = calendar
-        self.datetime_compatible = True
         self._dayofwk = dayofwk
         self._dayofyr = dayofyr
+        self.tzinfo = None
+        if calendar:
+            calendar = calendar.lower()
+        if calendar == 'gregorian' or calendar == 'standard':
+            # dates after 1582-10-15 can be converted to and compared to
+            # proleptic Gregorian dates
+            self.calendar = 'gregorian'
+            if self.to_tuple() >= (1582, 10, 15, 0, 0, 0, 0):
+                self.datetime_compatible = True
+            else:
+                self.datetime_compatible = False
+            assert_valid_date(self, is_leap_gregorian, True)
+            self.has_year_zero = False
+        elif calendar == 'noleap' or calendar == '365_day':
+            self.calendar = 'noleap'
+            self.datetime_compatible = False
+            assert_valid_date(self, no_leap, False, has_year_zero=True)
+            self.has_year_zero = True
+        elif calendar == 'all_leap' or calendar == '366_day':
+            self.calendar = 'all_leap'
+            self.datetime_compatible = False
+            assert_valid_date(self, all_leap, False, has_year_zero=True)
+            self.has_year_zero = True
+        elif calendar == '360_day':
+            self.calendar = calendar
+            self.datetime_compatible = False
+            assert_valid_date(self, no_leap, False, has_year_zero=True, is_360_day=True)
+            self.has_year_zero = True
+        elif calendar == 'julian':
+            self.calendar = calendar
+            self.datetime_compatible = False
+            assert_valid_date(self, is_leap_julian, False)
+            self.has_year_zero = False
+        elif calendar == 'proleptic_gregorian':
+            self.calendar = calendar
+            self.datetime_compatible = True
+            assert_valid_date(self, is_leap_proleptic_gregorian, False)
+            self.has_year_zero = False
+        elif calendar == '' or calendar is None:
+            # instance not calendar-aware, some method will not work
+            self.calendar = ''
+            self.datetime_compatible = False
+            self.has_year_zero = False
+        else:
+            raise ValueError(
+                "calendar must be one of %s, got '%s'" % (str(_calendars), calendar))
 
     @property
     def format(self):
@@ -881,9 +987,11 @@ Gregorial calendar.
 
     @property
     def dayofwk(self):
-        if self._dayofwk < 0 and self.calendar != '':
-            jd = _IntJulianDayFromDate(self.year,self.month,self.day,self.calendar)
-            year,month,day,dayofwk,dayofyr = _IntJulianDayToDate(jd,self.calendar)
+        if self._dayofwk < 0 and self.calendar:
+            jd = _IntJulianDayFromDate(self.year,self.month,self.day,self.calendar,
+                                       skip_transition=False,has_year_zero=self.has_year_zero)
+            year,month,day,dayofwk,dayofyr = _IntJulianDayToDate(jd,self.calendar,
+                                       skip_transition=False,has_year_zero=self.has_year_zero)
             # cache results for dayofwk, dayofyr
             self._dayofwk = dayofwk
             self._dayofyr = dayofyr
@@ -893,9 +1001,11 @@ Gregorial calendar.
 
     @property
     def dayofyr(self):
-        if self._dayofyr < 0 and self.calendar != '':
-            jd = _IntJulianDayFromDate(self.year,self.month,self.day,self.calendar)
-            year,month,day,dayofwk,dayofyr = _IntJulianDayToDate(jd,self.calendar)
+        if self._dayofyr < 0 and self.calendar:
+            jd = _IntJulianDayFromDate(self.year,self.month,self.day,self.calendar,
+                                       skip_transition=False,has_year_zero=self.has_year_zero)
+            year,month,day,dayofwk,dayofyr = _IntJulianDayToDate(jd,self.calendar,
+                                       skip_transition=False,has_year_zero=self.has_year_zero)
             # cache results for dayofwk, dayofyr
             self._dayofwk = dayofwk
             self._dayofyr = dayofyr
@@ -905,7 +1015,15 @@ Gregorial calendar.
 
     @property
     def daysinmonth(self):
-        return get_days_in_month(_is_leap(self.year,self.calendar), self.month)
+        if self.calendar == 'noleap':
+            return _dpm[self.month-1]
+        elif self.calendar == 'all_leap':
+            return _dpm_leap[self.month-1]
+        elif self.calendar == '360_day':
+            return _dpm_360[self.month-1]
+        else:
+            return get_days_in_month(_is_leap(self.year,self.calendar,
+                   has_year_zero=self.has_year_zero), self.month)
 
     def strftime(self, format=None):
         """
@@ -958,9 +1076,15 @@ Gregorial calendar.
                              self.microsecond)
 
     def __repr__(self):
-        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
-                                     self.__class__.__name__,
-                                     self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
+        if self.calendar == None:
+            return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8}, calendar={9})".format('cftime',
+            self.__class__.__name__,
+            self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond,self.calendar)
+        else:
+            return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8}, calendar='{9}')".format('cftime',
+            self.__class__.__name__,
+            self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond,self.calendar)
+
     def __str__(self):
         return self.isoformat(' ')
 
@@ -1058,13 +1182,36 @@ Gregorial calendar.
         cdef datetime dt
         if isinstance(self, datetime) and isinstance(other, timedelta):
             dt = self
+            calendar = self.calendar
             delta = other
         elif isinstance(self, timedelta) and isinstance(other, datetime):
             dt = other
+            calendar = other.calendar
             delta = self
         else:
             return NotImplemented
-        return dt._add_timedelta(delta)
+        # return calendar-specific subclasses for backward compatbility,
+        # even though after 1.3.0 this is no longer necessary.
+        if calendar == '360_day':
+            #return dt.__class__(*add_timedelta_360_day(dt, delta),calendar=calendar)
+            return Datetime360Day(*add_timedelta_360_day(dt, delta))
+        elif calendar == 'noleap':
+            #return dt.__class__(*add_timedelta(dt, delta, no_leap, False, True),calendar=calendar)
+            return DatetimeNoLeap(*add_timedelta(dt, delta, no_leap, False, True))
+        elif calendar == 'all_leap':
+            #return dt.__class__(*add_timedelta(dt, delta, all_leap, False, True),calendar=calendar)
+            return DatetimeAllLeap(*add_timedelta(dt, delta, all_leap, False, True))
+        elif calendar == 'julian':
+            #return dt.__class__(*add_timedelta(dt, delta, is_leap_julian, False, False),calendar=calendar)
+            return DatetimeJulian(*add_timedelta(dt, delta, is_leap_julian, False, False))
+        elif calendar == 'gregorian':
+            #return dt.__class__(*add_timedelta(dt, delta, is_leap_gregorian, True, False),calendar=calendar)
+            return DatetimeGregorian(*add_timedelta(dt, delta, is_leap_gregorian, True, False))
+        elif calendar == 'proleptic_gregorian':
+            #return dt.__class__(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, False),calendar=calendar)
+            return DatetimeProlepticGregorian(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, False))
+        else:
+            return NotImplemented
 
     def __sub__(self, other):
         cdef datetime dt
@@ -1076,8 +1223,10 @@ Gregorial calendar.
                     raise ValueError("cannot compute the time difference between dates with different calendars")
                 if dt.calendar == "":
                     raise ValueError("cannot compute the time difference between dates that are not calendar-aware")
-                ordinal_self = _IntJulianDayFromDate(dt.year, dt.month, dt.day, dt.calendar)
-                ordinal_other = _IntJulianDayFromDate(other.year, other.month, other.day, other.calendar)
+                ordinal_self = _IntJulianDayFromDate(dt.year, dt.month, dt.day, dt.calendar,
+                                       skip_transition=False,has_year_zero=self.has_year_zero)
+                ordinal_other = _IntJulianDayFromDate(other.year, other.month, other.day, other.calendar,
+                                       skip_transition=False,has_year_zero=self.has_year_zero)
                 days = ordinal_self - ordinal_other
                 seconds_self = dt.second + 60 * dt.minute + 3600 * dt.hour
                 seconds_other = other.second + 60 * other.minute + 3600 * other.hour
@@ -1096,7 +1245,29 @@ datetime object."""
                 return dt._to_real_datetime() - other
             elif isinstance(other, timedelta):
                 # datetime - timedelta
-                return dt._add_timedelta(-other)
+                # return calendar-specific subclasses for backward compatbility,
+                # even though after 1.3.0 this is no longer necessary.
+                if self.calendar == '360_day':
+                    #return self.__class__(*add_timedelta_360_day(self, -other),calendar=self.calendar)
+                    return Datetime360Day(*add_timedelta_360_day(self, -other))
+                elif self.calendar == 'noleap':
+                    #return self.__class__(*add_timedelta(self, -other, no_leap, False, True),calendar=self.calendar)
+                    return DatetimeNoLeap(*add_timedelta(self, -other, no_leap, False, True))
+                elif self.calendar == 'all_leap':
+                    #return self.__class__(*add_timedelta(self, -other, all_leap, False, True),calendar=self.calendar)
+                    return DatetimeAllLeap(*add_timedelta(self, -other, all_leap, False, True))
+                elif self.calendar == 'julian':
+                    #return self.__class__(*add_timedelta(self, -other, is_leap_julian, False, False),calendar=self.calendar)
+                    return DatetimeJulian(*add_timedelta(self, -other, is_leap_julian, False, False))
+                elif self.calendar == 'gregorian':
+                    #return self.__class__(*add_timedelta(self, -other, is_leap_gregorian, True, False),calendar=self.calendar)
+                    return DatetimeGregorian(*add_timedelta(self, -other, is_leap_gregorian, True, False))
+                elif self.calendar == 'proleptic_gregorian':
+                    #return self.__class__(*add_timedelta(self, -other,
+                    #    is_leap_proleptic_gregorian, False, False),calendar=self.calendar)
+                    return DatetimeProlepticGregorian(*add_timedelta(self, -other, is_leap_proleptic_gregorian, False, False))
+                else:
+                    return NotImplemented
             else:
                 return NotImplemented
         else:
@@ -1113,6 +1284,9 @@ datetime object."""
             else:
                 return NotImplemented
 
+# these calendar-specific sub-classes are no longer used, but stubs
+# remain for backward compatibility.
+
 @cython.embedsignature(True)
 cdef class DatetimeNoLeap(datetime):
     """
@@ -1120,17 +1294,12 @@ 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)
-        self.calendar = "noleap"
-        self.datetime_compatible = False
-        assert_valid_date(self, no_leap, False, has_year_zero=True)
-
-    cdef _add_timedelta(self, delta):
-        return DatetimeNoLeap(*add_timedelta(self, delta, no_leap, False, True))
-
-    @property
-    def daysinmonth(self):
-        return _dpm[self.month-1]
+        kwargs['calendar']='noleap'
+        super().__init__(*args, **kwargs)
+    def __repr__(self):
+        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
+                                     self.__class__.__name__,
+                                     self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
 
 @cython.embedsignature(True)
 cdef class DatetimeAllLeap(datetime):
@@ -1139,17 +1308,12 @@ 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)
-        self.calendar = "all_leap"
-        self.datetime_compatible = False
-        assert_valid_date(self, all_leap, False, has_year_zero=True)
-
-    cdef _add_timedelta(self, delta):
-        return DatetimeAllLeap(*add_timedelta(self, delta, all_leap, False, True))
-
-    @property
-    def daysinmonth(self):
-        return _dpm_leap[self.month-1]
+        kwargs['calendar']='all_leap'
+        super().__init__(*args, **kwargs)
+    def __repr__(self):
+        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
+                                     self.__class__.__name__,
+                                     self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
 
 @cython.embedsignature(True)
 cdef class Datetime360Day(datetime):
@@ -1158,17 +1322,12 @@ 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)
-        self.calendar = "360_day"
-        self.datetime_compatible = False
-        assert_valid_date(self, no_leap, False, has_year_zero=True, is_360_day=True)
-
-    cdef _add_timedelta(self, delta):
-        return Datetime360Day(*add_timedelta_360_day(self, delta))
-
-    @property
-    def daysinmonth(self):
-        return _dpm_360[self.month-1]
+        kwargs['calendar']='360_day'
+        super().__init__(*args, **kwargs)
+    def __repr__(self):
+        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
+                                     self.__class__.__name__,
+                                     self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
 
 @cython.embedsignature(True)
 cdef class DatetimeJulian(datetime):
@@ -1177,13 +1336,12 @@ Phony datetime object which mimics the python datetime object,
 but uses the "julian" calendar.
     """
     def __init__(self, *args, **kwargs):
-        datetime.__init__(self, *args, **kwargs)
-        self.calendar = "julian"
-        self.datetime_compatible = False
-        assert_valid_date(self, is_leap_julian, False)
-
-    cdef _add_timedelta(self, delta):
-        return DatetimeJulian(*add_timedelta(self, delta, is_leap_julian, False, False))
+        kwargs['calendar']='julian'
+        super().__init__(*args, **kwargs)
+    def __repr__(self):
+        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
+                                     self.__class__.__name__,
+                                     self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
 
 @cython.embedsignature(True)
 cdef class DatetimeGregorian(datetime):
@@ -1200,19 +1358,12 @@ 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)
-        self.calendar = "gregorian"
-
-        # dates after 1582-10-15 can be converted to and compared to
-        # proleptic Gregorian dates
-        if self.to_tuple() >= (1582, 10, 15, 0, 0, 0, 0):
-            self.datetime_compatible = True
-        else:
-            self.datetime_compatible = False
-        assert_valid_date(self, is_leap_gregorian, True)
-
-    cdef _add_timedelta(self, delta):
-        return DatetimeGregorian(*add_timedelta(self, delta, is_leap_gregorian, True, False))
+        kwargs['calendar']='gregorian'
+        super().__init__(*args, **kwargs)
+    def __repr__(self):
+        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
+                                     self.__class__.__name__,
+                                     self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
 
 @cython.embedsignature(True)
 cdef class DatetimeProlepticGregorian(datetime):
@@ -1233,14 +1384,12 @@ Instance variables are year,month,day,hour,minute,second,microsecond,dayofwk,day
 format, and calendar.
     """
     def __init__(self, *args, **kwargs):
-        datetime.__init__(self, *args, **kwargs)
-        self.calendar = "proleptic_gregorian"
-        self.datetime_compatible = True
-        assert_valid_date(self, is_leap_proleptic_gregorian, False)
-
-    cdef _add_timedelta(self, delta):
-        return DatetimeProlepticGregorian(*add_timedelta(self, delta,
-                                                         is_leap_proleptic_gregorian, False, False))
+        kwargs['calendar']='proleptic_gregorian'
+        super().__init__( *args, **kwargs)
+    def __repr__(self):
+        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
+                                     self.__class__.__name__,
+                                     self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
 
 _illegal_s = re.compile(r"((^|[^%])(%%)*%s)")
 
@@ -1291,7 +1440,7 @@ cdef _strftime(datetime dt, fmt):
             sites.append(site)
 
     s = s1
-    syear = "%4d" % (dt.year,)
+    syear = "%04d" % (dt.year,)
     for site in sites:
         s = s[:site] + syear + s[site + 4:]
     return s
@@ -1435,7 +1584,7 @@ cdef tuple add_timedelta(datetime dt, delta, bint (*is_leap)(int), bint julian_g
             if month > 12:
                 month = 1
                 year += 1
-                if year == 0:
+                if year == 0 and not has_year_zero:
                     year = 1
                 month_length = month_lengths(is_leap, year)
             day = 1
@@ -1490,16 +1639,16 @@ cdef tuple add_timedelta_360_day(datetime dt, delta):
 # Calendar calculations base on calcals.c by David W. Pierce
 # http://meteora.ucsd.edu/~pierce/calcalcs
 
-cdef _is_leap(int year, calendar):
+cdef _is_leap(int year, calendar, has_year_zero=False):
     cdef int tyear
     cdef bint leap
     calendar = _check_calendar(calendar)
-    if year == 0:
+    if year == 0 and not has_year_zero:
         raise ValueError('year zero does not exist in the %s calendar' %\
                 calendar)
     # Because there is no year 0 in the Julian calendar, years -1, -5, -9, etc
     # are leap years.
-    if year < 0:
+    if year < 0 and not has_year_zero:
         tyear = year + 1
     else:
         tyear = year
@@ -1520,7 +1669,7 @@ cdef _is_leap(int year, calendar):
         leap = False
     return leap
 
-cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=False):
+cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=False,has_year_zero=False):
     """Compute integer Julian Day from year,month,day and calendar.
 
     Allowed calendars are 'standard', 'gregorian', 'julian',
@@ -1542,7 +1691,7 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
     date is noon UTC 0-1-1 for other calendars.
 
     There is no year zero in standard (mixed), julian, or proleptic_gregorian
-    calendars.
+    calendars by default. If has_year_zero=True, then year zero is included.
 
     Subtract 0.5 to get 00 UTC on that day.
 
@@ -1570,18 +1719,18 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
         return _IntJulianDayFromDate_366day(year,month,day)
 
     # handle standard, julian, proleptic_gregorian calendars.
-    if year == 0:
+    if year == 0 and not has_year_zero:
         raise ValueError('year zero does not exist in the %s calendar' %\
                 calendar)
     if (calendar == 'proleptic_gregorian'         and year < -4714) or\
        (calendar in ['julian','standard']  and year < -4713):
         raise ValueError('year out of range for %s calendar' % calendar)
-    leap = _is_leap(year,calendar)
+    leap = _is_leap(year,calendar,has_year_zero=has_year_zero)
     if not leap and month == 2 and day == 29:
         raise ValueError('%s is not a leap year' % year)
 
     # add year offset
-    if year < 0:
+    if year < 0 and not has_year_zero:
         year += 4801
     else:
         year += 4800
@@ -1617,7 +1766,7 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
 
     return jday
 
-cdef _IntJulianDayToDate(int jday,calendar,skip_transition=False):
+cdef _IntJulianDayToDate(int jday,calendar,skip_transition=False,has_year_zero=False):
     """Compute the year,month,day,dow,doy given the integer Julian day.
     and calendar. (dow = day of week with 0=Mon,6=Sun and doy is day of year).
 
@@ -1666,18 +1815,18 @@ cdef _IntJulianDayToDate(int jday,calendar,skip_transition=False):
 
     # Advance years until we find the right one
     yp1 = year + 1
-    if yp1 == 0:
+    if yp1 == 0 and not has_year_zero:
        yp1 = 1 # no year 0
-    tjday = _IntJulianDayFromDate(yp1,1,1,calendar,skip_transition=True)
+    tjday = _IntJulianDayFromDate(yp1,1,1,calendar,skip_transition=True,has_year_zero=has_year_zero)
     while jday >= tjday:
         year += 1
-        if year == 0:
+        if year == 0 and not has_year_zero:
             year = 1
         yp1 = year + 1
-        if yp1 == 0:
+        if yp1 == 0 and not has_year_zero:
             yp1 = 1
-        tjday = _IntJulianDayFromDate(yp1,1,1,calendar,skip_transition=True)
-    if _is_leap(year, calendar):
+        tjday = _IntJulianDayFromDate(yp1,1,1,calendar,skip_transition=True,has_year_zero=has_year_zero)
+    if _is_leap(year, calendar,has_year_zero=has_year_zero):
         dpm2use = _dpm_leap
         spm2use = _spm_366day
     else:
@@ -1685,12 +1834,12 @@ cdef _IntJulianDayToDate(int jday,calendar,skip_transition=False):
         spm2use = _spm_365day
     month = 1
     tjday =\
-    _IntJulianDayFromDate(year,month,dpm2use[month-1],calendar,skip_transition=True)
+    _IntJulianDayFromDate(year,month,dpm2use[month-1],calendar,skip_transition=True,has_year_zero=has_year_zero)
     while jday > tjday:
         month += 1
         tjday =\
-        _IntJulianDayFromDate(year,month,dpm2use[month-1],calendar,skip_transition=True)
-    tjday = _IntJulianDayFromDate(year,month,1,calendar,skip_transition=True)
+        _IntJulianDayFromDate(year,month,dpm2use[month-1],calendar,skip_transition=True,has_year_zero=has_year_zero)
+    tjday = _IntJulianDayFromDate(year,month,1,calendar,skip_transition=True,has_year_zero=has_year_zero)
     day = jday - tjday + 1
     if month == 1:
         doy = day
@@ -2000,51 +2149,51 @@ class utime:
 Performs conversions of netCDF time coordinate
 data to/from datetime objects.
 
-To initialize: C{t = utime(unit_string,calendar='standard')}
+To initialize: `t = utime(unit_string,calendar='standard'`
 
 where
 
-B{C{unit_string}} is a string of the form
-C{'time-units since <time-origin>'} defining the time units.
+`unit_string` is a string of the form
+`time-units since <time-origin>` defining the time units.
 
 Valid time-units are days, hours, minutes and seconds (the singular forms
-are also accepted). An example unit_string would be C{'hours
-since 0001-01-01 00:00:00'}. months is allowed as a time unit
+are also accepted). An example unit_string would be `hours
+since 0001-01-01 00:00:00`. months is allowed as a time unit
 *only* for the 360_day calendar.
 
-The B{C{calendar}} keyword describes the calendar used in the time calculations.
+The calendar keyword describes the calendar used in the time calculations.
 All the values currently defined in the U{CF metadata convention
 <http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.1/cf-conventions.html#time-coordinate>}
-are accepted. The default is C{'standard'}, which corresponds to the mixed
-Gregorian/Julian calendar used by the C{udunits library}. Valid calendars
+are accepted. The default is 'standard', which corresponds to the mixed
+Gregorian/Julian calendar used by the udunits library. Valid calendars
 are:
 
-C{'gregorian'} or C{'standard'} (default):
+'gregorian' or 'standard' (default):
 
 Mixed Gregorian/Julian calendar as defined by udunits.
 
-C{'proleptic_gregorian'}:
+'proleptic_gregorian':
 
 A Gregorian calendar extended to dates before 1582-10-15. That is, a year
 is a leap year if either (i) it is divisible by 4 but not by 100 or (ii)
 it is divisible by 400.
 
-C{'noleap'} or C{'365_day'}:
+'noleap' or '365_day':
 
 Gregorian calendar without leap years, i.e., all years are 365 days long.
 all_leap or 366_day Gregorian calendar with every year being a leap year,
 i.e., all years are 366 days long.
 
-C{'360_day'}:
+'360_day':
 
 All years are 360 days divided into 30 day months.
 
-C{'julian'}:
+'julian':
 
 Proleptic Julian calendar, extended to dates after 1582-10-5. A year is a
 leap year if it is divisible by 4.
 
-The C{L{num2date}} and C{L{date2num}} class methods can used to convert datetime
+The num2date and date2num class methods can used to convert datetime
 instances to/from the specified time units using the specified calendar.
 
 Example usage:
@@ -2068,8 +2217,8 @@ Example usage:
 The resolution of the transformation operation is approximately a microsecond.
 
 Warning:  Dates between 1582-10-5 and 1582-10-15 do not exist in the
-C{'standard'} or C{'gregorian'} calendars.  An exception will be raised if you pass
-a 'datetime-like' object in that range to the C{L{date2num}} class method.
+'standard' or 'gregorian' calendars.  An exception will be raised if you pass
+a 'datetime-like' object in that range to the date2num class method.
 
 Words of Wisdom from the British MetOffice concerning reference dates:
 
@@ -2081,42 +2230,42 @@ that the reference date be later than 1582. If earlier dates must be used,
 it should be noted that udunits treats 0 AD as identical to 1 AD."
 
 @ivar origin: datetime instance defining the origin of the netCDF time variable.
- at ivar calendar:  the calendar used (as specified by the C{calendar} keyword).
+ at ivar calendar:  the calendar used (as specified by the `calendar` keyword).
 @ivar unit_string:  a string defining the the netCDF time variable.
- at ivar units:  the units part of C{unit_string} (i.e. 'days', 'hours', 'seconds').
+ at ivar units:  the units part of `unit_string` (i.e. 'days', 'hours', 'seconds').
     """
 
     def __init__(self, unit_string, calendar='standard',
                  only_use_cftime_datetimes=True,only_use_python_datetimes=False):
         """
 @param unit_string: a string of the form
-C{'time-units since <time-origin>'} defining the time units.
+`time-units since <time-origin>` defining the time units.
 
 Valid time-units are days, hours, minutes and seconds (the singular forms
-are also accepted). An example unit_string would be C{'hours
-since 0001-01-01 00:00:00'}. months is allowed as a time unit
+are also accepted). An example unit_string would be `hours
+since 0001-01-01 00:00:00`. months is allowed as a time unit
 *only* for the 360_day calendar.
 
 @keyword calendar: describes the calendar used in the time calculations.
 All the values currently defined in the U{CF metadata convention
 <http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.1/cf-conventions.html#time-coordinate>}
-are accepted. The default is C{'standard'}, which corresponds to the mixed
-Gregorian/Julian calendar used by the C{udunits library}. Valid calendars
+are accepted. The default is `standard`, which corresponds to the mixed
+Gregorian/Julian calendar used by the udunits library. Valid calendars
 are:
- - C{'gregorian'} or C{'standard'} (default):
+ - `gregorian` or `standard` (default):
  Mixed Gregorian/Julian calendar as defined by udunits.
- - C{'proleptic_gregorian'}:
+ - `proleptic_gregorian`:
  A Gregorian calendar extended to dates before 1582-10-15. That is, a year
  is a leap year if either (i) it is divisible by 4 but not by 100 or (ii)
  it is divisible by 400.
- - C{'noleap'} or C{'365_day'}:
+ - `noleap` or `365_day`:
  Gregorian calendar without leap years, i.e., all years are 365 days long.
- - C{'all_leap'} or C{'366_day'}:
+ - `all_leap` or `366_day`:
  Gregorian calendar with every year being a leap year, i.e.,
  all years are 366 days long.
- -C{'360_day'}:
+ -`360_day`:
  All years are 360 days divided into 30 day months.
- -C{'julian'}:
+ -`julian`:
  Proleptic Julian calendar, extended to dates after 1582-10-5. A year is a
  leap year if it is divisible by 4.
 
@@ -2146,8 +2295,8 @@ units to datetime objects.
 
     def date2num(self, date):
         """
-        Returns C{time_value} in units described by L{unit_string}, using
-        the specified L{calendar}, given a 'datetime-like' object.
+        Returns `time_value` in units described by `unit_string`, using
+        the specified `calendar`, given a 'datetime-like' object.
 
         The datetime object must represent UTC with no time-zone offset.
         If there is a time-zone offset implied by L{unit_string}, it will
@@ -2155,7 +2304,7 @@ units to datetime objects.
 
         Resolution is approximately a microsecond.
 
-        If C{calendar = 'standard'} or C{'gregorian'} (indicating
+        If calendar = 'standard' or 'gregorian' (indicating
         that the mixed Julian/Gregorian calendar is to be used), an
         exception will be raised if the 'datetime-like' object describes
         a date between 1582-10-5 and 1582-10-15.
@@ -2167,8 +2316,8 @@ units to datetime objects.
 
     def num2date(self, time_value):
         """
-        Return a 'datetime-like' object given a C{time_value} in units
-        described by L{unit_string}, using L{calendar}.
+        Return a 'datetime-like' object given a `time_value` in units
+        described by `unit_string`, using `calendar`.
 
         dates are in UTC with no offset, even if L{unit_string} contains
         a time zone offset from UTC.


=====================================
test/test_cftime.py
=====================================
@@ -51,6 +51,19 @@ est = timezone(timedelta(hours=-5), 'UTC')
 dtime = namedtuple('dtime', ('values', 'units', 'calendar'))
 dateformat =  '%Y-%m-%d %H:%M:%S'
 
+calendars=['standard', 'gregorian', 'proleptic_gregorian', 'noleap', 'julian',\
+           'all_leap', '365_day', '366_day', '360_day']
+def adjust_calendar(calendar):
+    # check for and remove calendar synonyms.
+    calendar = calendar.lower()
+    if calendar == 'gregorian' or calendar == 'standard':
+        return 'gregorian'
+    elif calendar == 'noleap' or calendar == '365_day':
+        return 'noleap'
+    elif calendar == 'all_leap' or calendar == '366_day':
+        return 'all_leap'
+    else:
+        return calendar
 
 class CFTimeVariable(object):
     '''dummy "netCDF" variable to hold time info'''
@@ -248,7 +261,7 @@ class cftimeTestCase(unittest.TestCase):
         # 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='')
         t1 = self.cdftime_360day.date2num(d)
         assert_almost_equal(t1, 360 * 400.)
         d2 = self.cdftime_360day.num2date(t1)
@@ -297,7 +310,7 @@ class cftimeTestCase(unittest.TestCase):
             t2 = date2num(d, units='days since 0001-01-01 00:00:00')
             assert(abs(t2 - t) < 1e-5)  # values should be less than second
         # Check equality testing
-        d1 = datetimex(1979, 6, 21, 9, 23, 12)
+        d1 = datetimex(1979, 6, 21, 9, 23, 12, calendar='standard')
         d2 = datetime(1979, 6, 21, 9, 23, 12)
         assert(d1 == d2)
         # check timezone offset
@@ -371,108 +384,55 @@ class cftimeTestCase(unittest.TestCase):
 
         # test rountrip accuracy
         # also tests error found in issue #349
-        calendars=['standard', 'gregorian', 'proleptic_gregorian', 'noleap', 'julian',\
-                   'all_leap', '365_day', '366_day', '360_day']
         dateref = datetime(2015,2,28,12)
-        ntimes = 1001
         verbose = True # print out max error diagnostics
+        ntimes = 101
+        def roundtrip(delta,eps,units):
+            times1 = date2num(dateref,units,calendar=calendar)
+            times1 += delta*np.arange(0,ntimes)
+            dates1 = num2date(times1,units,calendar=calendar)
+            times2 = date2num(dates1,units,calendar=calendar)
+            dates2 = num2date(times2,units,calendar=calendar)
+            err = np.abs(times1 - times2)
+            assert(np.all(err<eps))
+            dates1 = [date.strftime(dateformat) for date in dates1]
+            dates2 = [date.strftime(dateformat) for date in dates2]
+            assert(dates1==dates2)
+            return err.max()
         for calendar in calendars:
-            eps = 1.
+            eps = 1.; delta = 1.
             units = 'microseconds since 2000-01-30 01:01:01'
-            microsecs1 = date2num(dateref,units,calendar=calendar)
-            maxerr = 0
-            for n in range(ntimes):
-                microsecs1 += 1.
-                date1 = num2date(microsecs1, units, calendar=calendar)
-                microsecs2 = date2num(date1, units, calendar=calendar)
-                date2 = num2date(microsecs2, units, calendar=calendar)
-                err = np.abs(microsecs1 - microsecs2)
-                maxerr = max(err,maxerr)
-                assert(err < eps)
-                assert(date1.strftime(dateformat) == date2.strftime(dateformat))
+            maxerr = roundtrip(eps,delta,units)
             if verbose:
                 print('calendar = %s max abs err (microsecs) = %s eps = %s' % \
                      (calendar,maxerr,eps))
-            units = 'milliseconds since 1800-01-30 01:01:01'
-            eps = 0.001
-            millisecs1 = date2num(dateref,units,calendar=calendar)
-            maxerr = 0.
-            for n in range(ntimes):
-                millisecs1 += 0.001
-                date1 = num2date(millisecs1, units, calendar=calendar)
-                millisecs2 = date2num(date1, units, calendar=calendar)
-                date2 = num2date(millisecs2, units, calendar=calendar)
-                err = np.abs(millisecs1 - millisecs2)
-                maxerr = max(err,maxerr)
-                assert(err < eps)
-                assert(date1.strftime(dateformat) == date2.strftime(dateformat))
+            eps = 0.001; delta = 0.001
+            units = 'milliseconds since 2000-01-30 01:01:01'
+            maxerr = roundtrip(eps,delta,units)
             if verbose:
                 print('calendar = %s max abs err (millisecs) = %s eps = %s' % \
                      (calendar,maxerr,eps))
-            eps = 1.e-5
+            eps = 1.e-5; delta = 0.1
             units = 'seconds since 0001-01-30 01:01:01'
-            secs1 = date2num(dateref,units,calendar=calendar)
-            maxerr = 0.
-            for n in range(ntimes):
-                secs1 += 0.1
-                date1 = num2date(secs1, units, calendar=calendar)
-                secs2 = date2num(date1, units, calendar=calendar)
-                date2 = num2date(secs2, units, calendar=calendar)
-                err = np.abs(secs1 - secs2)
-                maxerr = max(err,maxerr)
-                assert(err < eps)
-                assert(date1.strftime(dateformat) == date2.strftime(dateformat))
+            maxerr = roundtrip(eps,delta,units)
             if verbose:
                 print('calendar = %s max abs err (secs) = %s eps = %s' % \
                      (calendar,maxerr,eps))
-            eps = 1.e-6
+            eps = 1.e-6; delta = 0.01
             units = 'minutes since 0001-01-30 01:01:01'
-            mins1 = date2num(dateref,units,calendar=calendar)
-            maxerr = 0.
-            for n in range(ntimes):
-                mins1 += 0.01
-                date1 = num2date(mins1, units, calendar=calendar)
-                mins2 = date2num(date1, units, calendar=calendar)
-                date2 = num2date(mins2, units, calendar=calendar)
-                err = np.abs(mins1 - mins2)
-                maxerr = max(err,maxerr)
-                assert(err < eps)
-                diff = abs(date1-date2)
-                assert(diff.microseconds < 100)
+            maxerr = roundtrip(eps,delta,units)
             if verbose:
                 print('calendar = %s max abs err (mins) = %s eps = %s' % \
                      (calendar,maxerr,eps))
-            eps = 1.e-8
+            eps = 1.e-8; delta = 0.001
             units = 'hours since 0001-01-30 01:01:01'
-            hrs1 = date2num(dateref,units,calendar=calendar)
-            maxerr = 0.
-            for n in range(ntimes):
-                hrs1 += 0.001
-                date1 = num2date(hrs1, units, calendar=calendar)
-                hrs2 = date2num(date1, units, calendar=calendar)
-                date2 = num2date(hrs2, units, calendar=calendar)
-                err = np.abs(hrs1 - hrs2)
-                maxerr = max(err,maxerr)
-                assert(err < eps)
-                diff = abs(date1-date2)
-                assert(diff.microseconds < 100)
+            maxerr = roundtrip(eps,delta,units)
             if verbose:
                 print('calendar = %s max abs err (hours) = %s eps = %s' % \
                      (calendar,maxerr,eps))
-            eps = 1.e-9
+            eps = 1.e-9; delta = 0.00001
             units = 'days since 0001-01-30 01:01:01'
-            days1 = date2num(dateref,units,calendar=calendar)
-            maxerr = 0.
-            for n in range(ntimes):
-                days1 += 0.00001
-                date1 = num2date(days1, units, calendar=calendar)
-                days2 = date2num(date1, units, calendar=calendar)
-                date2 = num2date(days2, units, calendar=calendar)
-                err = np.abs(days1 - days2)
-                maxerr = max(err,maxerr)
-                assert(err < eps)
-                diff = abs(date1-date2)
-                assert(diff.microseconds < 100)
+            maxerr = roundtrip(eps,delta,units)
             if verbose:
                 print('calendar = %s max abs err (days) = %s eps = %s' % \
                      (calendar,maxerr,eps))
@@ -767,7 +727,7 @@ class cftimeTestCase(unittest.TestCase):
         assert isinstance(d, datetime)
         # issue #169: cftime.datetime has no calendar attribute, causing dayofwk,dayofyr methods
         # to fail.
-        c = cftime.datetime(*cftime._parse_date('7480-01-01 00:00:00'))
+        c = cftime.datetime(*cftime._parse_date('7480-01-01 00:00:00'),calendar='standard')
         assert(c.strftime() == '7480-01-01 00:00:00')
         # issue #175: masked values not treated properly in num2date
         times = np.ma.masked_array([-3956.7499999995343,-999999999999],mask=[False,True])
@@ -777,7 +737,7 @@ class cftimeTestCase(unittest.TestCase):
         test = dates == np.ma.masked_array([datetime(1848, 1, 17, 6, 0, 0, 40), None],mask=[0,1])
         assert(test.all())
         dates = num2date(times, units=units, calendar='standard')
-        assert(str(dates)=='[cftime.DatetimeGregorian(1848, 1, 17, 6, 0, 0, 40) --]')
+        assert(str(dates)=="[cftime.DatetimeGregorian(1848, 1, 17, 6, 0, 0, 40) --]")
 #  check that time range of 200,000 + years can be represented accurately
         calendar='standard'
         _MAX_INT64 = np.iinfo("int64").max
@@ -811,7 +771,8 @@ class cftimeTestCase(unittest.TestCase):
 # issue #187 - roundtrip near second boundary
         dt1 = datetime(1810, 4, 24, 16, 15, 10)
         units = 'days since -4713-01-01 12:00'
-        dt2 = num2date(date2num(dt1, units), units)
+        dt2 = num2date(date2num(dt1, units), units, calendar='proleptic_gregorian')
+        dt2 = num2date(date2num(dt1, units, calendar='standard'), units)
         assert(dt1 == dt2)
 # issue #189 - leap years calculated incorrectly for negative years in proleptic_gregorian calendar
         dt1 = datetime(2020, 4, 24, 16, 15, 10)
@@ -819,6 +780,29 @@ class cftimeTestCase(unittest.TestCase):
         cal = 'proleptic_gregorian'
         dt2 = num2date(date2num(dt1, units, cal), units, cal)
         assert(dt1 == dt2)
+# issue #198 - cftime.datetime creates calendar specific datetimes that
+# support addition/subtraction of timedeltas.
+        for cal in calendars:
+            dt = cftime.datetime(2020, 1, 1, calendar=cal)
+            dt += timedelta(hours=1)
+            assert(str(dt) == '2020-01-01 01:00:00')
+# issue #193 - years with more than four digits in reference date
+        assert(cftime.date2num(cftime.datetime(18000, 12, 1, 0, 0), 'days since 18000-1-1', '360_day') == 330.0)
+        # julian day not including year zero
+        d = cftime.datetime(2020, 12, 1, 12, calendar='julian')
+        units = 'days since -4713-1-1 12:00'
+        jd = cftime.date2num(d,units,calendar='julian')
+        assert(jd == 2459198.0)
+        # if calendar=None, use input date to determine calendar
+        jd = cftime.date2num(d,units,calendar=None)
+        assert(jd == 2459198.0)
+        # if no calendar specified, use calendar associated with datetime
+        # instance.
+        jd = cftime.date2num(d,units)
+        assert(jd == 2459198.0)
+        # use 'standard' calendar
+        jd = cftime.date2num(d,units,calendar='standard')
+        assert(jd == 2459185.0)
 
 
 class TestDate2index(unittest.TestCase):
@@ -1554,7 +1538,8 @@ def test_num2date_only_use_cftime_datetimes_negative_years(
         calendar, expected_date_type):
     result = num2date(-1000., units='days since 0001-01-01', calendar=calendar,
                       only_use_cftime_datetimes=True)
-    assert isinstance(result, expected_date_type)
+    assert isinstance(result, datetimex)
+    assert (result.calendar == adjust_calendar(calendar))
 
 
 @pytest.mark.parametrize(
@@ -1565,7 +1550,8 @@ def test_num2date_only_use_cftime_datetimes_pre_gregorian(
         calendar, expected_date_type):
     result = num2date(1., units='days since 0001-01-01', calendar=calendar,
                       only_use_cftime_datetimes=True)
-    assert isinstance(result, expected_date_type)
+    assert isinstance(result, datetimex)
+    assert (result.calendar == adjust_calendar(calendar))
 
 
 @pytest.mark.parametrize(
@@ -1576,13 +1562,15 @@ def test_num2date_only_use_cftime_datetimes_post_gregorian(
         calendar, expected_date_type):
     result = num2date(0., units='days since 1582-10-15', calendar=calendar,
                       only_use_cftime_datetimes=True)
-    assert isinstance(result, expected_date_type)
+    assert isinstance(result, datetimex)
+    assert (result.calendar == adjust_calendar(calendar))
 
 
 def test_repr():
-    #expected = 'cftime.datetime(2000-01-01 00:00:00)'
-    expected = 'cftime.datetime(2000, 1, 1, 0, 0, 0, 0)'
-    assert repr(datetimex(2000, 1, 1)) == expected
+    expected = "cftime.datetime(2000, 1, 1, 0, 0, 0, 0, calendar='gregorian')"
+    assert repr(datetimex(2000, 1, 1, calendar='standard')) == expected
+    expected = "cftime.datetime(2000, 1, 1, 0, 0, 0, 0, calendar='')"
+    assert repr(datetimex(2000, 1, 1, calendar=None)) == expected
 
 
 def test_dayofyr_after_replace(date_type):



View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/commit/c370f74e5eff446271b9fdd22a8c925fe0cd1e2c

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/commit/c370f74e5eff446271b9fdd22a8c925fe0cd1e2c
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20201223/106e65b6/attachment-0001.html>


More information about the Pkg-grass-devel mailing list