Skip to content

Commit 85a4f4f

Browse files
committed
Ensure dates are rounded to nearest second
1 parent bd72437 commit 85a4f4f

File tree

2 files changed

+246
-2
lines changed

2 files changed

+246
-2
lines changed

cf_units/__init__.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,41 @@ def num2date(time_value, unit, calendar):
753753
if unit_string.endswith(" since epoch"):
754754
unit_string = unit_string.replace("epoch", EPOCH)
755755
cdftime = netcdftime.utime(unit_string, calendar=calendar)
756-
return cdftime.num2date(time_value)
756+
return _num2date_to_nearest_second(time_value, cdftime)
757+
758+
759+
def _num2date_to_nearest_second(time_value, utime):
760+
# Return datetime encoding of numeric time value with respect to the given
761+
# time reference units, with a resolution of 1 second.
762+
763+
# We account for the edge case where the time is in seconds and has a
764+
# half second: utime.num2date() may produce a date that would round
765+
# down.
766+
#
767+
# Note that this behaviour is different to the num2date function in older
768+
# versions of netcdftime that didn't have microsecond precision. In those
769+
# versions, a half-second value would be rounded up or down arbitrarily. It
770+
# is probably not possible to replicate that behaviour with the current
771+
# version (1.4.1), if one wished to do so for the sake of consistency.
772+
has_half_second = utime.units == 'seconds' and \
773+
time_value % 1. == 0.5
774+
date = utime.num2date(time_value)
775+
try:
776+
microsecond = date.microsecond
777+
except AttributeError:
778+
microsecond = 0
779+
if has_half_second or microsecond > 0:
780+
if has_half_second or microsecond >= 500000:
781+
seconds = Unit('second')
782+
second_frac = seconds.convert(0.75, utime.units)
783+
time_value += second_frac
784+
date = utime.num2date(time_value)
785+
# Create a date object of the same type returned by utime.num2date()
786+
# (either datetime.datetime or netcdftime.datetime), discarding the
787+
# microseconds
788+
date = date.__class__(date.year, date.month, date.day,
789+
date.hour, date.minute, date.second)
790+
return date
757791

758792

759793
########################################################################
@@ -2078,4 +2112,4 @@ def num2date(self, time_value):
20782112
20792113
"""
20802114
cdf_utime = self.utime()
2081-
return cdf_utime.num2date(time_value)
2115+
return _num2date_to_nearest_second(time_value, cdf_utime)
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# (C) British Crown Copyright 2016, Met Office
2+
#
3+
# This file is part of cf_units.
4+
#
5+
# cf_units is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# cf_units is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with cf_units. If not, see <http://www.gnu.org/licenses/>.
17+
"""Test function :func:`cf_units._num2date_to_nearest_second`."""
18+
19+
from __future__ import (absolute_import, division, print_function)
20+
from six.moves import (filter, input, map, range, zip) # noqa
21+
22+
import unittest
23+
import datetime
24+
25+
import numpy as np
26+
import netcdftime
27+
28+
from cf_units import _num2date_to_nearest_second, Unit
29+
30+
31+
class Test(unittest.TestCase):
32+
def setup_units(self, calendar):
33+
self.useconds = netcdftime.utime('seconds since 1970-01-01', calendar)
34+
self.uminutes = netcdftime.utime('minutes since 1970-01-01', calendar)
35+
self.uhours = netcdftime.utime('hours since 1970-01-01', calendar)
36+
self.udays = netcdftime.utime('days since 1970-01-01', calendar)
37+
38+
def check_dates(self, nums, utimes, expected):
39+
for num, utime, exp in zip(nums, utimes, expected):
40+
res = _num2date_to_nearest_second(num, utime)
41+
self.assertEqual(exp, res)
42+
43+
# Gregorian Calendar tests
44+
45+
def test_simple_gregorian(self):
46+
self.setup_units('gregorian')
47+
nums = [20., 40.,
48+
75., 150.,
49+
8., 16.,
50+
300., 600.]
51+
utimes = [self.useconds, self.useconds,
52+
self.uminutes, self.uminutes,
53+
self.uhours, self.uhours,
54+
self.udays, self.udays]
55+
expected = [datetime.datetime(1970, 1, 1, 0, 0, 20),
56+
datetime.datetime(1970, 1, 1, 0, 0, 40),
57+
datetime.datetime(1970, 1, 1, 1, 15),
58+
datetime.datetime(1970, 1, 1, 2, 30),
59+
datetime.datetime(1970, 1, 1, 8),
60+
datetime.datetime(1970, 1, 1, 16),
61+
datetime.datetime(1970, 10, 28),
62+
datetime.datetime(1971, 8, 24)]
63+
64+
self.check_dates(nums, utimes, expected)
65+
66+
def test_fractional_gregorian(self):
67+
self.setup_units('gregorian')
68+
nums = [5./60., 10./60.,
69+
15./60., 30./60.,
70+
8./24., 16./24.]
71+
utimes = [self.uminutes, self.uminutes,
72+
self.uhours, self.uhours,
73+
self.udays, self.udays]
74+
expected = [datetime.datetime(1970, 1, 1, 0, 0, 5),
75+
datetime.datetime(1970, 1, 1, 0, 0, 10),
76+
datetime.datetime(1970, 1, 1, 0, 15),
77+
datetime.datetime(1970, 1, 1, 0, 30),
78+
datetime.datetime(1970, 1, 1, 8),
79+
datetime.datetime(1970, 1, 1, 16)]
80+
81+
self.check_dates(nums, utimes, expected)
82+
83+
def test_fractional_second_gregorian(self):
84+
self.setup_units('gregorian')
85+
nums = [0.25, 0.5, 0.75,
86+
1.5, 2.5, 3.5, 4.5]
87+
utimes = [self.useconds] * 7
88+
expected = [datetime.datetime(1970, 1, 1, 0, 0, 0),
89+
datetime.datetime(1970, 1, 1, 0, 0, 1),
90+
datetime.datetime(1970, 1, 1, 0, 0, 1),
91+
datetime.datetime(1970, 1, 1, 0, 0, 2),
92+
datetime.datetime(1970, 1, 1, 0, 0, 3),
93+
datetime.datetime(1970, 1, 1, 0, 0, 4),
94+
datetime.datetime(1970, 1, 1, 0, 0, 5)]
95+
96+
self.check_dates(nums, utimes, expected)
97+
98+
# 360 day Calendar tests
99+
100+
def test_simple_360_day(self):
101+
self.setup_units('360_day')
102+
nums = [20., 40.,
103+
75., 150.,
104+
8., 16.,
105+
300., 600.]
106+
utimes = [self.useconds, self.useconds,
107+
self.uminutes, self.uminutes,
108+
self.uhours, self.uhours,
109+
self.udays, self.udays]
110+
expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 20),
111+
netcdftime.datetime(1970, 1, 1, 0, 0, 40),
112+
netcdftime.datetime(1970, 1, 1, 1, 15),
113+
netcdftime.datetime(1970, 1, 1, 2, 30),
114+
netcdftime.datetime(1970, 1, 1, 8),
115+
netcdftime.datetime(1970, 1, 1, 16),
116+
netcdftime.datetime(1970, 11, 1),
117+
netcdftime.datetime(1971, 9, 1)]
118+
119+
self.check_dates(nums, utimes, expected)
120+
121+
def test_fractional_360_day(self):
122+
self.setup_units('360_day')
123+
nums = [5./60., 10./60.,
124+
15./60., 30./60.,
125+
8./24., 16./24.]
126+
utimes = [self.uminutes, self.uminutes,
127+
self.uhours, self.uhours,
128+
self.udays, self.udays]
129+
expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 5),
130+
netcdftime.datetime(1970, 1, 1, 0, 0, 10),
131+
netcdftime.datetime(1970, 1, 1, 0, 15),
132+
netcdftime.datetime(1970, 1, 1, 0, 30),
133+
netcdftime.datetime(1970, 1, 1, 8),
134+
netcdftime.datetime(1970, 1, 1, 16)]
135+
136+
self.check_dates(nums, utimes, expected)
137+
138+
def test_fractional_second_360_day(self):
139+
self.setup_units('360_day')
140+
nums = [0.25, 0.5, 0.75,
141+
1.5, 2.5, 3.5, 4.5]
142+
utimes = [self.useconds] * 7
143+
expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 0),
144+
netcdftime.datetime(1970, 1, 1, 0, 0, 1),
145+
netcdftime.datetime(1970, 1, 1, 0, 0, 1),
146+
netcdftime.datetime(1970, 1, 1, 0, 0, 2),
147+
netcdftime.datetime(1970, 1, 1, 0, 0, 3),
148+
netcdftime.datetime(1970, 1, 1, 0, 0, 4),
149+
netcdftime.datetime(1970, 1, 1, 0, 0, 5)]
150+
151+
self.check_dates(nums, utimes, expected)
152+
153+
# 365 day Calendar tests
154+
155+
def test_simple_365_day(self):
156+
self.setup_units('365_day')
157+
nums = [20., 40.,
158+
75., 150.,
159+
8., 16.,
160+
300., 600.]
161+
utimes = [self.useconds, self.useconds,
162+
self.uminutes, self.uminutes,
163+
self.uhours, self.uhours,
164+
self.udays, self.udays]
165+
expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 20),
166+
netcdftime.datetime(1970, 1, 1, 0, 0, 40),
167+
netcdftime.datetime(1970, 1, 1, 1, 15),
168+
netcdftime.datetime(1970, 1, 1, 2, 30),
169+
netcdftime.datetime(1970, 1, 1, 8),
170+
netcdftime.datetime(1970, 1, 1, 16),
171+
netcdftime.datetime(1970, 10, 28),
172+
netcdftime.datetime(1971, 8, 24)]
173+
174+
self.check_dates(nums, utimes, expected)
175+
176+
def test_fractional_365_day(self):
177+
self.setup_units('365_day')
178+
nums = [5./60., 10./60.,
179+
15./60., 30./60.,
180+
8./24., 16./24.]
181+
utimes = [self.uminutes, self.uminutes,
182+
self.uhours, self.uhours,
183+
self.udays, self.udays]
184+
185+
expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 5),
186+
netcdftime.datetime(1970, 1, 1, 0, 0, 10),
187+
netcdftime.datetime(1970, 1, 1, 0, 15),
188+
netcdftime.datetime(1970, 1, 1, 0, 30),
189+
netcdftime.datetime(1970, 1, 1, 8),
190+
netcdftime.datetime(1970, 1, 1, 16)]
191+
192+
self.check_dates(nums, utimes, expected)
193+
194+
def test_fractional_second_365_day(self):
195+
self.setup_units('365_day')
196+
nums = [0.25, 0.5, 0.75,
197+
1.5, 2.5, 3.5, 4.5]
198+
utimes = [self.useconds] * 7
199+
expected = [netcdftime.datetime(1970, 1, 1, 0, 0, 0),
200+
netcdftime.datetime(1970, 1, 1, 0, 0, 1),
201+
netcdftime.datetime(1970, 1, 1, 0, 0, 1),
202+
netcdftime.datetime(1970, 1, 1, 0, 0, 2),
203+
netcdftime.datetime(1970, 1, 1, 0, 0, 3),
204+
netcdftime.datetime(1970, 1, 1, 0, 0, 4),
205+
netcdftime.datetime(1970, 1, 1, 0, 0, 5)]
206+
207+
self.check_dates(nums, utimes, expected)
208+
209+
if __name__ == '__main__':
210+
unittest.main()

0 commit comments

Comments
 (0)