Skip to content
Closed
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
16 changes: 6 additions & 10 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,17 +558,13 @@ def _repr_other_metadata(self):
return result

def _str_dates(self, dates_as_numbers):
date_obj_array = self.units.num2date(dates_as_numbers)
date_obj_array = np.array(
[iris.util._num2date_to_nearest_second(num, self.units)
for num in dates_as_numbers])
kwargs = {'separator': ', ', 'prefix': ' '}
try:
# With NumPy 1.7 we need to ask for 'str' formatting.
result = np.core.arrayprint.array2string(
date_obj_array, formatter={'numpystr': str}, **kwargs)
except TypeError:
# But in 1.6 we don't need to ask, and the option doesn't
# even exist!
result = np.core.arrayprint.array2string(date_obj_array, **kwargs)
return result
return np.core.arrayprint.array2string(date_obj_array,
formatter={'numpystr': str},
**kwargs)

def __str__(self):
if self.units.is_time_reference():
Expand Down
6 changes: 3 additions & 3 deletions lib/iris/fileformats/grib/_save_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from iris.fileformats.grib import grib_phenom_translation as gptx
from iris.fileformats.grib._load_convert import (_STATISTIC_TYPE_NAMES,
_TIME_RANGE_UNITS)
from iris.util import is_regular, regular_step
from iris.util import is_regular, regular_step, _num2date_to_nearest_second


# Invert code tables from :mod:`iris.fileformats.grib._load_convert`.
Expand Down Expand Up @@ -604,7 +604,7 @@ def _missing_forecast_period(cube):
t = t_coord.bounds[0, 0] if t_coord.has_bounds() else t_coord.points[0]
frt = frt_coord.points[0]
# Calculate GRIB parameters.
rt = frt_coord.units.num2date(frt)
rt = _num2date_to_nearest_second(frt, frt_coord.units)
rt_meaning = 1 # Forecast reference time.
fp = t - frt
integer_fp = int(fp)
Expand All @@ -619,7 +619,7 @@ def _missing_forecast_period(cube):
# reference time significance of "Observation time" and set the
# forecast period to 0h.
t = t_coord.bounds[0, 0] if t_coord.has_bounds() else t_coord.points[0]
rt = t_coord.units.num2date(t)
rt = _num2date_to_nearest_second(t, t_coord.units)
rt_meaning = 3 # Observation time
fp = 0
fp_meaning = 1 # Hours
Expand Down
6 changes: 4 additions & 2 deletions lib/iris/pandas.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2013 - 2015, Met Office
# (C) British Crown Copyright 2013 - 2016, Met Office
#
# This file is part of Iris.
#
Expand Down Expand Up @@ -35,6 +35,7 @@
import iris
from iris.coords import AuxCoord, DimCoord
from iris.cube import Cube
from iris.util import _num2date_to_nearest_second


def _add_iris_coord(cube, name, points, dim, calendar=None):
Expand Down Expand Up @@ -119,7 +120,8 @@ def _as_pandas_coord(coord):
"""Convert an Iris Coord into a Pandas index or columns array."""
index = coord.points
if coord.units.is_time_reference():
index = coord.units.num2date(index)
index = np.array([_num2date_to_nearest_second(point, coord.units)
for point in index])
return index


Expand Down
3 changes: 2 additions & 1 deletion lib/iris/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from iris.exceptions import IrisError
# Importing iris.palette to register the brewer palettes.
import iris.palette
from iris.util import _num2date_to_nearest_second


# Cynthia Brewer citation text.
Expand Down Expand Up @@ -407,7 +408,7 @@ def _fixup_dates(coord, values):
if coord.units.calendar is not None and values.ndim == 1:
# Convert coordinate values into tuples of
# (year, month, day, hour, min, sec)
dates = [coord.units.num2date(val).timetuple()[0:6]
dates = [_num2date_to_nearest_second(val, coord.units).timetuple()[0:6]
for val in values]
if coord.units.calendar == 'gregorian':
r = [datetime.datetime(*date) for date in dates]
Expand Down
214 changes: 214 additions & 0 deletions lib/iris/tests/unit/util/test__num2date_to_nearest_second.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# (C) British Crown Copyright 2016, Met Office
#
# This file is part of Iris.
#
# Iris 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.
#
# Iris 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 Iris. If not, see <http://www.gnu.org/licenses/>.
"""Test function :func:`iris.util._num2date_to_nearest_second`."""

from __future__ import (absolute_import, division, print_function)
from six.moves import (filter, input, map, range, zip) # noqa

# import iris tests first so that some things can be initialised before
# importing anything else
import iris.tests as tests

import datetime

from cf_units import Unit
import numpy as np
import netcdftime

from iris.util import _num2date_to_nearest_second


class Test(tests.IrisTest):
def setup_units(self, calendar):
self.useconds = Unit('seconds since epoch', calendar)
self.uminutes = Unit('minutes since epoch', calendar)
self.uhours = Unit('hours since epoch', calendar)
self.udays = Unit('days since epoch', calendar)

def check_dates(self, nums, units, expected):
for num, unit, exp in zip(nums, units, expected):
res = _num2date_to_nearest_second(num, unit)
self.assertEqual(exp, res)

# Gregorian Calendar tests

def test_simple_gregorian(self):
self.setup_units('gregorian')
nums = [20., 40.,
75., 150.,
8., 16.,
300., 600.]
units = [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, units, expected)

def test_fractional_gregorian(self):
self.setup_units('gregorian')
nums = [5./60., 10./60.,
15./60., 30./60.,
8./24., 16./24.]
units = [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, units, 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]
units = [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, units, 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.]
units = [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, units, 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.]
units = [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, units, 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]
units = [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, units, 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.]
units = [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, units, 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.]
units = [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, units, 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]
units = [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, units, expected)

if __name__ == '__main__':
tests.main()
32 changes: 32 additions & 0 deletions lib/iris/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1589,3 +1589,35 @@ def demote_dim_coord_to_aux_coord(cube, name_or_coord):
cube.remove_coord(dim_coord)

cube.add_aux_coord(dim_coord, coord_dim)


def _num2date_to_nearest_second(time_value, units):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this function may prove to be useful for users experiencing issues with netcdf4 times

I think that it is worth considering making it public and adapting the first few comment lines as the docstring.

I would likely leave the note that this behaviour... as a developer comment and not include it in teh docstring in this case

# Return datetime encoding of numeric time value with respect to the given
# time reference units, with a resolution of 1 second.

# We account for the edge case where the time is in seconds and has a
# half second: units.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_second = units.utime().units == 'seconds' and \
time_value % 1. == 0.5
date = units.num2date(time_value)
try:
microsecond = date.microsecond
except AttributeError:
microsecond = 0
if has_half_second or microsecond > 0:
if has_half_second or microsecond >= 500000:
seconds = cf_units.Unit('second')
second_frac = seconds.convert(0.75, units.utime().units)
time_value += second_frac
date = units.num2date(time_value)
date = date.__class__(date.year, date.month, date.day,
date.hour, date.minute, date.second)

return date