[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