diff --git a/cf_units/__init__.py b/cf_units/__init__.py index 4fecbf46..2666dbee 100644 --- a/cf_units/__init__.py +++ b/cf_units/__init__.py @@ -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): @@ -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): @@ -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. @@ -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): diff --git a/cf_units/tests/integration/test_date2num.py b/cf_units/tests/integration/test_date2num.py index 8e8a406b..aa31f21e 100644 --- a/cf_units/tests/integration/test_date2num.py +++ b/cf_units/tests/integration/test_date2num.py @@ -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. # @@ -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() diff --git a/cf_units/tests/unit/unit/test_Unit.py b/cf_units/tests/unit/unit/test_Unit.py index 3a87872b..0589bed6 100644 --- a/cf_units/tests/unit/unit/test_Unit.py +++ b/cf_units/tests/unit/unit/test_Unit.py @@ -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. # @@ -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()