Skip to content

Commit

Permalink
Merge 9b34931 into 71edd69
Browse files Browse the repository at this point in the history
  • Loading branch information
jswhit committed May 28, 2020
2 parents 71edd69 + 9b34931 commit ef6dc7d
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 40 deletions.
6 changes: 6 additions & 0 deletions Changelog
@@ -1,3 +1,9 @@
version 1.1.4 (not yet released)
================================
* fix treatment of masked arrays in num2date and date2num (issue #175).
Also make sure masked arrays are output from num2date/date2num if
masked arrays are input.

version 1.1.3 (release tag v1.1.3rel)
=====================================
* add isoformat method for compatibility with python datetime (issue #152).
Expand Down
123 changes: 84 additions & 39 deletions cftime/_cftime.pyx
Expand Up @@ -52,7 +52,7 @@ cdef int32_t* days_per_month_array = [
_rop_lookup = {Py_LT: '__gt__', Py_LE: '__ge__', Py_EQ: '__eq__',
Py_GT: '__lt__', Py_GE: '__le__', Py_NE: '__ne__'}

__version__ = '1.1.3'
__version__ = '1.1.4'

# Adapted from http://delete.me.uk/2005/03/iso8601.html
# Note: This regex ensures that all ISO8601 timezone formats are accepted - but, due to legacy support for other timestrings, not all incorrect formats can be rejected.
Expand Down Expand Up @@ -209,15 +209,15 @@ def date2num(dates,units,calendar='standard'):
dates[0]
except:
isscalar = True
ismasked = False
if np.ma.isMA(dates) and np.ma.is_masked(dates):
mask = dates.mask
ismasked = True
if isscalar:
dates = np.array([dates])
else:
dates = np.array(dates)
shape = dates.shape
ismasked = False
if np.ma.isMA(dates) and np.ma.is_masked(dates):
mask = dates.mask
ismasked = True
times = []
for date in dates.flat:
if getattr(date, 'tzinfo',None) is not None:
Expand All @@ -243,6 +243,13 @@ def date2num(dates,units,calendar='standard'):
times.append(totaltime/1.e6/3600./24.)
else:
raise ValueError('unsupported time units')
if ismasked: # convert to masked array if input was masked array
times = np.array(times)
times = np.ma.masked_where(times==None,times)
if isscalar:
return times[0]
else:
return np.reshape(times, shape)
if isscalar:
return times[0]
else:
Expand Down Expand Up @@ -343,18 +350,19 @@ def num2date(times,units,calendar='standard',\
times[0]
except:
isscalar = True
ismasked = False
if np.ma.isMA(times) and np.ma.is_masked(times):
mask = times.mask
ismasked = True
if isscalar:
times = np.array([times],dtype='d')
else:
times = np.array(times, dtype='d')
shape = times.shape
ismasked = False
if np.ma.isMA(times) and np.ma.is_masked(times):
mask = times.mask
ismasked = True
dates = []
n = 0
for time in times.flat:
if ismasked and not time:
if ismasked and mask.flat[n]:
dates.append(None)
else:
# convert to total seconds
Expand Down Expand Up @@ -386,6 +394,14 @@ def num2date(times,units,calendar='standard',\
OverflowError in python datetime, probably because year < datetime.MINYEAR"""
raise ValueError(msg)
dates.append(date)
n += 1
if ismasked: # convert to masked array if input was masked array
dates = np.array(dates)
dates = np.ma.masked_where(dates==None,dates)
if isscalar:
return dates[0]
else:
return np.reshape(dates, shape)
if isscalar:
return dates[0]
else:
Expand Down Expand Up @@ -812,6 +828,26 @@ units to datetime objects.
self._jd0 = JulianDayFromDate(self.origin, calendar=self.calendar)
self.only_use_cftime_datetimes = only_use_cftime_datetimes

def _convertunits(self, jdelta):
# convert julian day to desired units, add time zone offset.
if self.units in microsec_units:
jdelta = jdelta * 86400. * 1.e6 + self.tzoffset * 60. * 1.e6
elif self.units in millisec_units:
jdelta = jdelta * 86400. * 1.e3 + self.tzoffset * 60. * 1.e3
elif self.units in sec_units:
jdelta = jdelta * 86400. + self.tzoffset * 60.
elif self.units in min_units:
jdelta = jdelta * 1440. + self.tzoffset
elif self.units in hr_units:
jdelta = jdelta * 24. + self.tzoffset / 60.
elif self.units in day_units:
jdelta = jdelta + self.tzoffset / 1440.
elif self.units in month_units and self.calendar == '360_day':
jdelta = jdelta/30. + self.tzoffset / (30. * 1440.)
else:
raise ValueError('unsupported time units')
return jdelta

def date2num(self, date):
"""
Returns C{time_value} in units described by L{unit_string}, using
Expand All @@ -836,36 +872,39 @@ units to datetime objects.
date[0]
except:
isscalar = True
ismasked = False
if np.ma.isMA(date) and np.ma.is_masked(date):
mask = date.mask
ismasked = True
if not isscalar:
date = np.array(date)
shape = date.shape
if isscalar:
jdelta = JulianDayFromDate(date, self.calendar)-self._jd0
else:
jdelta = JulianDayFromDate(date.flat, self.calendar)-self._jd0
if not isscalar:
jdelta = np.array(jdelta)
# convert to desired units, add time zone offset.
if self.units in microsec_units:
jdelta = jdelta * 86400. * 1.e6 + self.tzoffset * 60. * 1.e6
elif self.units in millisec_units:
jdelta = jdelta * 86400. * 1.e3 + self.tzoffset * 60. * 1.e3
elif self.units in sec_units:
jdelta = jdelta * 86400. + self.tzoffset * 60.
elif self.units in min_units:
jdelta = jdelta * 1440. + self.tzoffset
elif self.units in hr_units:
jdelta = jdelta * 24. + self.tzoffset / 60.
elif self.units in day_units:
jdelta = jdelta + self.tzoffset / 1440.
elif self.units in month_units and self.calendar == '360_day':
jdelta = jdelta/30. + self.tzoffset / (30. * 1440.)
else:
raise ValueError('unsupported time units')
if isscalar:
return jdelta.astype(np.float64)
if ismasked:
jd = []
for d, m in zip(date.flat, mask.flat):
if not m:
jdelta = JulianDayFromDate(d, self.calendar)-self._jd0
jdelta = self._convertunits(jdelta)
else:
jdelta = None
jd.append(jdelta)
jd = np.array(jd)
jd = np.ma.masked_where(jd==None,jd, dtype=np.float64)
if isscalar:
return jd[0]
else:
return np.reshape(jd, shape)
else:
return np.reshape(jdelta.astype(np.float64), shape)
if isscalar:
jdelta = JulianDayFromDate(date, self.calendar)-self._jd0
else:
jdelta = JulianDayFromDate(date.flat, self.calendar)-self._jd0
jdelta = np.array(jdelta)
jdelta = self._convertunits(jdelta)
if isscalar:
return jdelta.astype(np.float64)
else:
return np.reshape(jdelta.astype(np.float64), shape)

def num2date(self, time_value):
"""
Expand Down Expand Up @@ -938,6 +977,13 @@ units to datetime objects.
else:
date = DateFromJulianDay(jd, self.calendar,
self.only_use_cftime_datetimes)
if ismasked: # convert to masked array if input was masked array
date = np.array(date)
date = np.ma.masked_where(date==None,date)
if isscalar:
return date[0]
else:
return np.reshape(date, shape)
if isscalar:
return date
else:
Expand Down Expand Up @@ -1348,10 +1394,9 @@ Gregorial calendar.
self.microsecond)

def __repr__(self):
return "{0}.{1}({2})".format('cftime',
return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7})".format('cftime',
self.__class__.__name__,
str(self))

self.year,self.month,self.day,self.hour,self.second,self.microsecond)
def __str__(self):
return self.isoformat(' ')

Expand Down
12 changes: 11 additions & 1 deletion test/test_cftime.py
Expand Up @@ -758,6 +758,15 @@ def test_tz_naive(self):
# to fail.
c = cftime.datetime(*cftime._parse_date('7480-01-01 00:00:00'))
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])
units='days since 1858-11-17 00:00:00'
dates = num2date(times, units=units, calendar='standard',\
only_use_cftime_datetimes=False, only_use_python_datetimes=True)
assert((dates==[datetime(1848, 1, 17, 6, 0, 0, 40),None]).all())
dates = num2date(times, units=units, calendar='standard')
assert((dates==[cftime.DatetimeGregorian(1848, 1, 17, 6, 0, 0),None]).all())


class TestDate2index(unittest.TestCase):

Expand Down Expand Up @@ -1514,7 +1523,8 @@ def test_num2date_only_use_cftime_datetimes_post_gregorian(


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


Expand Down

0 comments on commit ef6dc7d

Please sign in to comment.