Skip to content

Commit

Permalink
Gracefully handle long time intervals (#72)
Browse files Browse the repository at this point in the history
* Gracefully handle long time intervals

* Review actions: typo

* License headers...
  • Loading branch information
DPeterK authored and lbdreyer committed Feb 14, 2017
1 parent 9ac82d4 commit bd988a4
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 10 deletions.
51 changes: 43 additions & 8 deletions cf_units/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,9 +685,8 @@ def date2num(date, unit, calendar):
unit_string = unit.rstrip(" UTC")
if unit_string.endswith(" since epoch"):
unit_string = unit_string.replace("epoch", EPOCH)
cdftime = netcdftime.utime(unit_string, calendar=calendar)
date = _discard_microsecond(date)
return cdftime.date2num(date)
unit_inst = Unit(unit_string, calendar=calendar)
return unit_inst.date2num(date)


def _discard_microsecond(date):
Expand Down Expand Up @@ -785,8 +784,8 @@ def num2date(time_value, unit, calendar):
unit_string = unit.rstrip(" UTC")
if unit_string.endswith(" since epoch"):
unit_string = unit_string.replace("epoch", EPOCH)
cdftime = netcdftime.utime(unit_string, calendar=calendar)
return _num2date_to_nearest_second(time_value, cdftime)
unit_inst = Unit(unit_string, calendar=calendar)
return unit_inst.num2date(time_value)


def _num2date_to_nearest_second(time_value, utime):
Expand Down Expand Up @@ -1210,6 +1209,35 @@ def is_time_reference(self):
"""
return self.calendar is not None

def is_long_time_interval(self):
"""
Defines whether this unit describes a time unit with a long time
interval ("months" or "years"). These long time intervals *are*
supported by `UDUNITS2` but are not supported by `netcdftime`. This
discrepancy means we cannot run self.num2date() on a time unit with
a long time interval.
Returns:
Boolean.
For example:
>>> import cf_units
>>> u = cf_units.Unit('days since epoch')
>>> u.is_long_time_interval()
False
>>> u = cf_units.Unit('years since epoch')
>>> u.is_long_time_interval()
True
"""
result = False
long_time_intervals = ['year', 'month']
if self.is_time_reference():
result = any(interval in self.origin
for interval in long_time_intervals)
return result

def title(self, value):
"""
Return the unit value as a title string.
Expand Down Expand Up @@ -2069,13 +2097,20 @@ def utime(self):
datetime.datetime(1970, 1, 1, 2, 0)
"""
if self.calendar is None:
raise ValueError('Unit has undefined calendar')

# `netcdftime` cannot parse long time intervals ("months" or "years").
if self.is_long_time_interval():
interval = self.origin.split(' ')[0]
emsg = ('Time units with interval of "months", "years" '
'(or singular of these) cannot be processed, got {!r}.')
raise ValueError(emsg.format(interval))

#
# ensure to strip out non-parsable 'UTC' postfix which
# ensure to strip out non-parsable 'UTC' postfix, which
# is generated by UDUNITS-2 formatted output
#
if self.calendar is None:
raise ValueError('Unit has undefined calendar')
return netcdftime.utime(str(self).rstrip(" UTC"), self.calendar)

def date2num(self, date):
Expand Down
10 changes: 9 additions & 1 deletion cf_units/tests/integration/test_date2num.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2016, Met Office
# (C) British Crown Copyright 2016 - 2017, Met Office
#
# This file is part of cf_units.
#
Expand Down Expand Up @@ -70,6 +70,14 @@ def test_discard_mircosecond(self):

self.assertAlmostEqual(exp, res, places=4)

def test_long_time_interval(self):
# This test should fail with an error that we need to catch properly.
unit = 'years since 1970-01-01'
date = datetime.datetime(1970, 1, 1, 0, 0, 5)
exp_emsg = 'interval of "months", "years" .* got \'years\'.'
with self.assertRaisesRegexp(ValueError, exp_emsg):
date2num(date, unit, self.calendar)


if __name__ == '__main__':
unittest.main()
28 changes: 27 additions & 1 deletion cf_units/tests/unit/unit/test_Unit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2015 - 2016, Met Office
# (C) British Crown Copyright 2015 - 2017, Met Office
#
# This file is part of cf_units.
#
Expand Down Expand Up @@ -217,5 +217,31 @@ def test_type_conversion(self):
np.testing.assert_array_almost_equal(self.rads_array, result)


class Test_is_long_time_interval(unittest.TestCase):

def test_short_time_interval(self):
# A short time interval is a time interval from seconds to days.
unit = Unit('seconds since epoch')
result = unit.is_long_time_interval()
self.assertFalse(result)

def test_long_time_interval(self):
# A long time interval is a time interval of months or years.
unit = Unit('months since epoch')
result = unit.is_long_time_interval()
self.assertTrue(result)

def test_calendar(self):
# Check that a different calendar does not affect the result.
unit = Unit('months since epoch', calendar=cf_units.CALENDAR_360_DAY)
result = unit.is_long_time_interval()
self.assertTrue(result)

def test_not_time_unit(self):
unit = Unit('K')
result = unit.is_long_time_interval()
self.assertFalse(result)


if __name__ == '__main__':
unittest.main()

0 comments on commit bd988a4

Please sign in to comment.