Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix num2date / date2num multidimensional arrays. Remove iterator support. Fix masked arrays #68

Merged
merged 3 commits into from
Sep 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 23 additions & 24 deletions cf_units/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -703,16 +703,20 @@ def _discard_microsecond(date):
Date value/s

Returns:
datetime, or list of datetime object.
datetime, or numpy.ndarray of datetime object.

"""
is_scalar = False
if not hasattr(date, '__iter__'):
date = [date]
is_scalar = True
dates = [d.__class__(d.year, d.month, d.day, d.hour, d.minute, d.second)
for d in date]
return dates[0] if is_scalar else dates
dates = np.asarray(date)
shape = dates.shape
dates = dates.ravel()
# Create date objects of the same type returned by utime.num2date()
# (either datetime.datetime or netcdftime.datetime), discarding the
# microseconds
dates = np.array([d and d.__class__(d.year, d.month, d.day,
d.hour, d.minute, d.second)
for d in dates])
result = dates[0] if shape is () else dates.reshape(shape)
return result


def num2date(time_value, unit, calendar):
Expand Down Expand Up @@ -798,27 +802,25 @@ def _num2date_to_nearest_second(time_value, utime):
Returns:
datetime, or numpy.ndarray of datetime object.
"""
is_scalar = False
if not hasattr(time_value, '__iter__'):
time_value = [time_value]
is_scalar = True
time_values = np.array(list(time_value))
time_values = np.asanyarray(time_value)
shape = time_values.shape
time_values = time_values.ravel()

# We account for the edge case where the time is in seconds and has a
# half second: utime.num2date() may produce a date that would round
# down.
#
# Note that this behaviour is different to the num2date function in older
# versions of netcdftime that didn't have microsecond precision. In those
# versions, a half-second value would be rounded up or down arbitrarily. It
# is probably not possible to replicate that behaviour with the current
# version (1.4.1), if one wished to do so for the sake of consistency.
# Note that this behaviour is different to the num2date function in version
# 1.1 and earlier of netcdftime that didn't have microsecond precision. In
# those versions, a half-second value would be rounded up or down
# arbitrarily. It is probably not possible to replicate that behaviour with
# later versions, if one wished to do so for the sake of consistency.
has_half_seconds = np.logical_and(utime.units == 'seconds',
time_values % 1. == 0.5)
dates = utime.num2date(time_values)
try:
# We can assume all or none of the dates have a microsecond attribute
microseconds = np.array([d.microsecond for d in dates])
microseconds = np.array([d.microsecond if d else 0 for d in dates])
except AttributeError:
microseconds = 0
round_mask = np.logical_or(has_half_seconds, microseconds != 0)
Expand All @@ -827,12 +829,9 @@ def _num2date_to_nearest_second(time_value, utime):
useconds = Unit('second')
second_frac = useconds.convert(0.75, utime.units)
dates[ceil_mask] = utime.num2date(time_values[ceil_mask] + second_frac)
# Create date objects of the same type returned by utime.num2date()
# (either datetime.datetime or netcdftime.datetime), discarding the
# microseconds
dates[round_mask] = _discard_microsecond(dates[round_mask])

return dates[0] if is_scalar else dates
result = dates[0] if shape is () else dates.reshape(shape)
return result


########################################################################
Expand Down
17 changes: 13 additions & 4 deletions cf_units/tests/integration/test__num2date_to_nearest_second.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,20 @@ def test_sequence(self):
res = _num2date_to_nearest_second(nums, utime)
np.testing.assert_array_equal(exp, res)

def test_iter(self):
def test_multidim_sequence(self):
utime = netcdftime.utime('seconds since 1970-01-01', 'gregorian')
nums = iter([5., 10.])
exp = [datetime.datetime(1970, 1, 1, 0, 0, 5),
datetime.datetime(1970, 1, 1, 0, 0, 10)]
nums = [[20., 40., 60.],
[80, 100., 120.]]
exp_shape = (2, 3)
res = _num2date_to_nearest_second(nums, utime)
self.assertEqual(exp_shape, res.shape)

def test_masked_ndarray(self):
utime = netcdftime.utime('seconds since 1970-01-01', 'gregorian')
nums = np.ma.masked_array([20., 40., 60.], [False, True, False])
exp = [datetime.datetime(1970, 1, 1, 0, 0, 20),
None,
datetime.datetime(1970, 1, 1, 0, 1)]
res = _num2date_to_nearest_second(nums, utime)
np.testing.assert_array_equal(exp, res)

Expand Down
11 changes: 11 additions & 0 deletions cf_units/tests/integration/test_date2num.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ def test_sequence(self):
res = date2num(dates, self.unit, self.calendar)
np.testing.assert_array_almost_equal(exp, res, decimal=4)

def test_multidim_sequence(self):
dates = [[datetime.datetime(1970, 1, 1, 0, 0, 20),
datetime.datetime(1970, 1, 1, 0, 0, 40),
datetime.datetime(1970, 1, 1, 0, 1)],
[datetime.datetime(1970, 1, 1, 0, 1, 20),
datetime.datetime(1970, 1, 1, 0, 1, 40),
datetime.datetime(1970, 1, 1, 0, 2)]]
exp_shape = (2, 3)
res = date2num(dates, self.unit, self.calendar)
self.assertEqual(exp_shape, res.shape)

def test_discard_mircosecond(self):
date = datetime.datetime(1970, 1, 1, 0, 0, 5, 750000)
exp = 5.
Expand Down