Skip to content

Commit 5aa890a

Browse files
authored
Persist calendar when discarding cftime microsecond (#183)
* persist calendar when discarding cftime microsecond * review actions * review actions
1 parent 5abf5e2 commit 5aa890a

File tree

4 files changed

+129
-12
lines changed

4 files changed

+129
-12
lines changed

cf_units/__init__.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -403,20 +403,14 @@ def _discard_microsecond(date):
403403
datetime, or numpy.ndarray of datetime object.
404404
405405
"""
406-
dates = np.asarray(date)
406+
dates = np.asanyarray(date)
407407
shape = dates.shape
408408
dates = dates.ravel()
409-
# Create date objects of the same type returned by cftime.num2date()
410-
# (either datetime.datetime or cftime.datetime), discarding the
411-
# microseconds
412-
dates = np.array(
413-
[
414-
d
415-
and d.__class__(d.year, d.month, d.day, d.hour, d.minute, d.second)
416-
for d in dates
417-
]
418-
)
409+
410+
# using the "and" pattern to support masked arrays of datetimes
411+
dates = np.array([dt and dt.replace(microsecond=0) for dt in dates])
419412
result = dates[0] if shape == () else dates.reshape(shape)
413+
420414
return result
421415

422416

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Copyright cf-units contributors
2+
#
3+
# This file is part of cf-units and is released under the LGPL license.
4+
# See COPYING and COPYING.LESSER in the root of the repository for full
5+
# licensing details.
6+
"""Unit tests for the `cf_units._discard_microsecond` function."""
7+
8+
import datetime
9+
import unittest
10+
11+
import cftime
12+
import numpy as np
13+
import numpy.ma as ma
14+
15+
from cf_units import _discard_microsecond as discard_microsecond
16+
17+
18+
class Test__datetime(unittest.TestCase):
19+
def setUp(self):
20+
self.kwargs = dict(year=1, month=2, day=3, hour=4, minute=5, second=6)
21+
self.expected = datetime.datetime(**self.kwargs, microsecond=0)
22+
23+
def test_single(self):
24+
dt = datetime.datetime(**self.kwargs, microsecond=7)
25+
actual = discard_microsecond(dt)
26+
self.assertEqual(self.expected, actual)
27+
28+
def test_multi(self):
29+
shape = (5, 2)
30+
n = np.prod(shape)
31+
32+
dates = np.array(
33+
[datetime.datetime(**self.kwargs, microsecond=i) for i in range(n)]
34+
).reshape(shape)
35+
actual = discard_microsecond(dates)
36+
expected = np.array([self.expected] * n).reshape(shape)
37+
np.testing.assert_array_equal(expected, actual)
38+
39+
40+
class Test__cftime(unittest.TestCase):
41+
def setUp(self):
42+
self.kwargs = dict(year=1, month=2, day=3, hour=4, minute=5, second=6)
43+
self.calendars = cftime._cftime._calendars
44+
45+
def test_single(self):
46+
for calendar in self.calendars:
47+
dt = cftime.datetime(**self.kwargs, calendar=calendar)
48+
actual = discard_microsecond(dt)
49+
expected = cftime.datetime(
50+
**self.kwargs, microsecond=0, calendar=calendar
51+
)
52+
self.assertEqual(expected, actual)
53+
54+
def test_multi(self):
55+
shape = (2, 5)
56+
n = np.prod(shape)
57+
58+
for calendar in self.calendars:
59+
dates = np.array(
60+
[
61+
cftime.datetime(**self.kwargs, calendar=calendar)
62+
for i in range(n)
63+
]
64+
).reshape(shape)
65+
actual = discard_microsecond(dates)
66+
expected = np.array(
67+
[
68+
cftime.datetime(
69+
**self.kwargs, microsecond=0, calendar=calendar
70+
)
71+
]
72+
* n
73+
).reshape(shape)
74+
np.testing.assert_array_equal(expected, actual)
75+
76+
77+
class Test__falsy(unittest.TestCase):
78+
def setUp(self):
79+
self.kwargs = dict(year=1, month=2, day=3, hour=4, minute=5, second=6)
80+
self.calendar = "360_day"
81+
microsecond = 7
82+
self.cftime = cftime.datetime(
83+
**self.kwargs, microsecond=microsecond, calendar=self.calendar
84+
)
85+
self.datetime = datetime.datetime(
86+
**self.kwargs, microsecond=microsecond
87+
)
88+
89+
def test_single__none(self):
90+
self.assertIsNone(discard_microsecond(None))
91+
92+
def test_single__false(self):
93+
self.assertFalse(discard_microsecond(False))
94+
95+
def test_multi__falsy(self):
96+
falsy = np.array([None, False, 0])
97+
actual = discard_microsecond(falsy)
98+
np.testing.assert_array_equal(falsy, actual)
99+
100+
def test_masked(self):
101+
data = [self.cftime, self.datetime, self.cftime, self.datetime]
102+
mask = [1, 0, 0, 1]
103+
dates = ma.masked_array(data, mask=mask)
104+
actual = discard_microsecond(dates)
105+
expected = np.array(
106+
[
107+
ma.masked,
108+
datetime.datetime(**self.kwargs),
109+
cftime.datetime(**self.kwargs, calendar=self.calendar),
110+
ma.masked,
111+
]
112+
)
113+
self.assertEqual(expected.shape, actual.shape)
114+
for i, masked in enumerate(mask):
115+
if masked:
116+
self.assertIs(expected[i], actual[i])
117+
else:
118+
self.assertEqual(expected[i], actual[i])
119+
120+
121+
if __name__ == "__main__":
122+
unittest.main()

cf_units/tests/unit/unit/test_Unit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_non_gregorian_calendar_conversion_dtype(self):
6767
(np.float64, True),
6868
(np.int32, False),
6969
(np.int64, False),
70-
(np.int, False),
70+
(int, False),
7171
):
7272
data = np.arange(4, dtype=start_dtype)
7373
u1 = Unit("hours since 2000-01-01 00:00:00", calendar="360_day")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ exclude = '''
6262
| build
6363
| dist
6464
)/
65+
| _version.py
6566
)
6667
'''
6768

0 commit comments

Comments
 (0)