diff --git a/cf_units/__init__.py b/cf_units/__init__.py
index 1f61a09a..17542838 100644
--- a/cf_units/__init__.py
+++ b/cf_units/__init__.py
@@ -686,9 +686,35 @@ def date2num(date, unit, calendar):
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)
+def _discard_microsecond(date):
+ """
+ Return a date with the microsecond componenet discarded.
+
+ Works for scalars, sequences and numpy arrays. Returns a scalar
+ if input is a scalar, else returns a numpy array.
+
+ Args:
+
+ * date (datetime.datetime or netcdftime.datetime):
+ Date value/s
+
+ Returns:
+ datetime, or list 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
+
+
def num2date(time_value, unit, calendar):
"""
Return datetime encoding of numeric time value (resolution of 1 second).
@@ -714,6 +740,9 @@ def num2date(time_value, unit, calendar):
do not contain a time-zone offset, even if the specified unit
contains one.
+ Works for scalars, sequences and numpy arrays. Returns a scalar
+ if input is a scalar, else returns a numpy array.
+
Args:
* time_value (float):
@@ -753,7 +782,57 @@ def num2date(time_value, unit, calendar):
if unit_string.endswith(" since epoch"):
unit_string = unit_string.replace("epoch", EPOCH)
cdftime = netcdftime.utime(unit_string, calendar=calendar)
- return cdftime.num2date(time_value)
+ return _num2date_to_nearest_second(time_value, cdftime)
+
+
+def _num2date_to_nearest_second(time_value, utime):
+ """
+ Return datetime encoding of numeric time value with respect to the given
+ time reference units, with a resolution of 1 second.
+
+ * time_value (float):
+ Numeric time value/s.
+ * utime (netcdftime.utime):
+ netcdf.utime object with which to perform the conversion/s.
+
+ 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))
+
+ # 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.
+ 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])
+ except AttributeError:
+ microseconds = 0
+ round_mask = np.logical_or(has_half_seconds, microseconds != 0)
+ ceil_mask = np.logical_or(has_half_seconds, microseconds >= 500000)
+ if time_values[ceil_mask].size > 0:
+ 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
########################################################################
@@ -2036,6 +2115,7 @@ def date2num(self, date):
"""
cdf_utime = self.utime()
+ date = _discard_microsecond(date)
return cdf_utime.date2num(date)
def num2date(self, time_value):
@@ -2078,4 +2158,4 @@ def num2date(self, time_value):
"""
cdf_utime = self.utime()
- return cdf_utime.num2date(time_value)
+ return _num2date_to_nearest_second(time_value, cdf_utime)
diff --git a/cf_units/tests/integration/__init__.py b/cf_units/tests/integration/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cf_units/tests/integration/test__num2date_to_nearest_second.py b/cf_units/tests/integration/test__num2date_to_nearest_second.py
new file mode 100644
index 00000000..6c161d7f
--- /dev/null
+++ b/cf_units/tests/integration/test__num2date_to_nearest_second.py
@@ -0,0 +1,237 @@
+# (C) British Crown Copyright 2016, Met Office
+#
+# This file is part of cf_units.
+#
+# cf_units is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cf_units is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with cf_units. If not, see .
+"""Test function :func:`cf_units._num2date_to_nearest_second`."""
+
+from __future__ import (absolute_import, division, print_function)
+from six.moves import (filter, input, map, range, zip) # noqa
+
+import unittest
+import datetime
+
+import numpy as np
+import numpy.testing
+import netcdftime
+
+from cf_units import _num2date_to_nearest_second, Unit
+
+
+class Test(unittest.TestCase):
+ def setup_units(self, calendar):
+ self.useconds = netcdftime.utime('seconds since 1970-01-01', calendar)
+ self.uminutes = netcdftime.utime('minutes since 1970-01-01', calendar)
+ self.uhours = netcdftime.utime('hours since 1970-01-01', calendar)
+ self.udays = netcdftime.utime('days since 1970-01-01', calendar)
+
+ def check_dates(self, nums, utimes, expected):
+ for num, utime, exp in zip(nums, utimes, expected):
+ res = _num2date_to_nearest_second(num, utime)
+ self.assertEqual(exp, res)
+
+ def test_scalar(self):
+ utime = netcdftime.utime('seconds since 1970-01-01', 'gregorian')
+ num = 5.
+ exp = datetime.datetime(1970, 1, 1, 0, 0, 5)
+ res = _num2date_to_nearest_second(num, utime)
+ self.assertEqual(exp, res)
+
+ def test_sequence(self):
+ utime = netcdftime.utime('seconds since 1970-01-01', 'gregorian')
+ nums = [20., 40., 60., 80, 100.]
+ exp = [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)]
+ res = _num2date_to_nearest_second(nums, utime)
+ np.testing.assert_array_equal(exp, res)
+
+ def test_iter(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)]
+ res = _num2date_to_nearest_second(nums, utime)
+ np.testing.assert_array_equal(exp, res)
+
+ # Gregorian Calendar tests
+
+ def test_simple_gregorian(self):
+ self.setup_units('gregorian')
+ nums = [20., 40.,
+ 75., 150.,
+ 8., 16.,
+ 300., 600.]
+ utimes = [self.useconds, self.useconds,
+ self.uminutes, self.uminutes,
+ self.uhours, self.uhours,
+ self.udays, self.udays]
+ expected = [datetime.datetime(1970, 1, 1, 0, 0, 20),
+ datetime.datetime(1970, 1, 1, 0, 0, 40),
+ datetime.datetime(1970, 1, 1, 1, 15),
+ datetime.datetime(1970, 1, 1, 2, 30),
+ datetime.datetime(1970, 1, 1, 8),
+ datetime.datetime(1970, 1, 1, 16),
+ datetime.datetime(1970, 10, 28),
+ datetime.datetime(1971, 8, 24)]
+
+ self.check_dates(nums, utimes, expected)
+
+ def test_fractional_gregorian(self):
+ self.setup_units('gregorian')
+ nums = [5./60., 10./60.,
+ 15./60., 30./60.,
+ 8./24., 16./24.]
+ utimes = [self.uminutes, self.uminutes,
+ self.uhours, self.uhours,
+ self.udays, self.udays]
+ expected = [datetime.datetime(1970, 1, 1, 0, 0, 5),
+ datetime.datetime(1970, 1, 1, 0, 0, 10),
+ datetime.datetime(1970, 1, 1, 0, 15),
+ datetime.datetime(1970, 1, 1, 0, 30),
+ datetime.datetime(1970, 1, 1, 8),
+ datetime.datetime(1970, 1, 1, 16)]
+
+ self.check_dates(nums, utimes, expected)
+
+ def test_fractional_second_gregorian(self):
+ self.setup_units('gregorian')
+ nums = [0.25, 0.5, 0.75,
+ 1.5, 2.5, 3.5, 4.5]
+ utimes = [self.useconds] * 7
+ expected = [datetime.datetime(1970, 1, 1, 0, 0, 0),
+ datetime.datetime(1970, 1, 1, 0, 0, 1),
+ datetime.datetime(1970, 1, 1, 0, 0, 1),
+ datetime.datetime(1970, 1, 1, 0, 0, 2),
+ datetime.datetime(1970, 1, 1, 0, 0, 3),
+ datetime.datetime(1970, 1, 1, 0, 0, 4),
+ datetime.datetime(1970, 1, 1, 0, 0, 5)]
+
+ self.check_dates(nums, utimes, expected)
+
+ # 360 day Calendar tests
+
+ def test_simple_360_day(self):
+ self.setup_units('360_day')
+ nums = [20., 40.,
+ 75., 150.,
+ 8., 16.,
+ 300., 600.]
+ utimes = [self.useconds, self.useconds,
+ self.uminutes, self.uminutes,
+ self.uhours, self.uhours,
+ self.udays, self.udays]
+ expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 20),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 40),
+ netcdftime.datetime(1970, 1, 1, 1, 15),
+ netcdftime.datetime(1970, 1, 1, 2, 30),
+ netcdftime.datetime(1970, 1, 1, 8),
+ netcdftime.datetime(1970, 1, 1, 16),
+ netcdftime.datetime(1970, 11, 1),
+ netcdftime.datetime(1971, 9, 1)]
+
+ self.check_dates(nums, utimes, expected)
+
+ def test_fractional_360_day(self):
+ self.setup_units('360_day')
+ nums = [5./60., 10./60.,
+ 15./60., 30./60.,
+ 8./24., 16./24.]
+ utimes = [self.uminutes, self.uminutes,
+ self.uhours, self.uhours,
+ self.udays, self.udays]
+ expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 5),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 10),
+ netcdftime.datetime(1970, 1, 1, 0, 15),
+ netcdftime.datetime(1970, 1, 1, 0, 30),
+ netcdftime.datetime(1970, 1, 1, 8),
+ netcdftime.datetime(1970, 1, 1, 16)]
+
+ self.check_dates(nums, utimes, expected)
+
+ def test_fractional_second_360_day(self):
+ self.setup_units('360_day')
+ nums = [0.25, 0.5, 0.75,
+ 1.5, 2.5, 3.5, 4.5]
+ utimes = [self.useconds] * 7
+ expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 0),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 1),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 1),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 2),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 3),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 4),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 5)]
+
+ self.check_dates(nums, utimes, expected)
+
+ # 365 day Calendar tests
+
+ def test_simple_365_day(self):
+ self.setup_units('365_day')
+ nums = [20., 40.,
+ 75., 150.,
+ 8., 16.,
+ 300., 600.]
+ utimes = [self.useconds, self.useconds,
+ self.uminutes, self.uminutes,
+ self.uhours, self.uhours,
+ self.udays, self.udays]
+ expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 20),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 40),
+ netcdftime.datetime(1970, 1, 1, 1, 15),
+ netcdftime.datetime(1970, 1, 1, 2, 30),
+ netcdftime.datetime(1970, 1, 1, 8),
+ netcdftime.datetime(1970, 1, 1, 16),
+ netcdftime.datetime(1970, 10, 28),
+ netcdftime.datetime(1971, 8, 24)]
+
+ self.check_dates(nums, utimes, expected)
+
+ def test_fractional_365_day(self):
+ self.setup_units('365_day')
+ nums = [5./60., 10./60.,
+ 15./60., 30./60.,
+ 8./24., 16./24.]
+ utimes = [self.uminutes, self.uminutes,
+ self.uhours, self.uhours,
+ self.udays, self.udays]
+
+ expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 5),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 10),
+ netcdftime.datetime(1970, 1, 1, 0, 15),
+ netcdftime.datetime(1970, 1, 1, 0, 30),
+ netcdftime.datetime(1970, 1, 1, 8),
+ netcdftime.datetime(1970, 1, 1, 16)]
+
+ self.check_dates(nums, utimes, expected)
+
+ def test_fractional_second_365_day(self):
+ self.setup_units('365_day')
+ nums = [0.25, 0.5, 0.75,
+ 1.5, 2.5, 3.5, 4.5]
+ utimes = [self.useconds] * 7
+ expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 0),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 1),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 1),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 2),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 3),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 4),
+ netcdftime.datetime(1970, 1, 1, 0, 0, 5)]
+
+ self.check_dates(nums, utimes, expected)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/cf_units/tests/integration/test_date2num.py b/cf_units/tests/integration/test_date2num.py
new file mode 100644
index 00000000..1ce9de9b
--- /dev/null
+++ b/cf_units/tests/integration/test_date2num.py
@@ -0,0 +1,64 @@
+# (C) British Crown Copyright 2016, Met Office
+#
+# This file is part of cf_units.
+#
+# cf_units is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# cf_units is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with cf_units. If not, see .
+"""Test function :func:`cf_units.date2num`."""
+
+from __future__ import (absolute_import, division, print_function)
+from six.moves import (filter, input, map, range, zip) # noqa
+
+import unittest
+import datetime
+
+import numpy as np
+import numpy.testing
+import netcdftime
+
+from cf_units import date2num, Unit
+
+
+class Test(unittest.TestCase):
+ def setUp(self):
+ self.unit = 'seconds since 1970-01-01'
+ self.calendar = 'gregorian'
+
+ def test_single(self):
+ date = datetime.datetime(1970, 1, 1, 0, 0, 5)
+ exp = 5.
+ res = date2num(date, self.unit, self.calendar)
+ # num2date won't return an exact value representing the date,
+ # even if one exists
+ self.assertAlmostEqual(exp, res, places=4)
+
+ def test_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)]
+ exp = [20., 40., 60., 80, 100.]
+ res = date2num(dates, self.unit, self.calendar)
+ np.testing.assert_array_almost_equal(exp, res, decimal=4)
+
+ def test_discard_mircosecond(self):
+ date = datetime.datetime(1970, 1, 1, 0, 0, 5, 750000)
+ exp = 5.
+ res = date2num(date, self.unit, self.calendar)
+
+ self.assertAlmostEqual(exp, res, places=4)
+
+
+if __name__ == '__main__':
+ unittest.main()