Skip to content

Commit 855077c

Browse files
authored
num2date: return datetimes by default if possible (#165)
* make proper datetimes default * replace cftime.utime.num2date with cftime.num2date * attempt fixes * pass both utime and Unit to _num2date_to_nearest_second * revert _num2date_to_nearest_second calling sequence * dummy commit to kick travis * appease stickler * license headers * contain only_use_cftime_datetimes within utime object * revert test__num2date_to_nearest_second.py * actually pass flag to where it is needed * reinstate type check * Reinstate _num2date_to_nearest_second changes This reverts commit 0b89777. * reinstate test changes * make sure option is passed down num2date
1 parent 4cc94b1 commit 855077c

3 files changed

Lines changed: 96 additions & 23 deletions

File tree

cf_units/__init__.py

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ def _discard_microsecond(date):
484484
dates = np.asarray(date)
485485
shape = dates.shape
486486
dates = dates.ravel()
487-
# Create date objects of the same type returned by utime.num2date()
487+
# Create date objects of the same type returned by cftime.num2date()
488488
# (either datetime.datetime or cftime.datetime), discarding the
489489
# microseconds
490490
dates = np.array([d and d.__class__(d.year, d.month, d.day,
@@ -494,7 +494,7 @@ def _discard_microsecond(date):
494494
return result
495495

496496

497-
def num2date(time_value, unit, calendar):
497+
def num2date(time_value, unit, calendar, only_use_cftime_datetimes=False):
498498
"""
499499
Return datetime encoding of numeric time value (resolution of 1 second).
500500
@@ -508,7 +508,7 @@ def num2date(time_value, unit, calendar):
508508
unit = 'days since 001-01-01 00:00:00'}
509509
calendar = 'proleptic_gregorian'.
510510
511-
The datetime instances returned are 'real' python datetime
511+
By default, the datetime instances returned are 'real' python datetime
512512
objects if the date falls in the Gregorian calendar (i.e.
513513
calendar='proleptic_gregorian', or calendar = 'standard' or 'gregorian'
514514
and the date is after 1582-10-15). Otherwise, they are 'phony' datetime
@@ -535,6 +535,13 @@ def num2date(time_value, unit, calendar):
535535
* calendar (string):
536536
Name of the calendar, see cf_units.CALENDARS.
537537
538+
Kwargs:
539+
540+
* only_use_cftime_datetimes (bool):
541+
If True, will always return cftime datetime objects, regardless of
542+
calendar. If False, returns datetime.datetime instances where
543+
possible. Defaults to False.
544+
538545
Returns:
539546
datetime, or numpy.ndarray of datetime object.
540547
@@ -561,10 +568,12 @@ def num2date(time_value, unit, calendar):
561568
if unit_string.endswith(" since epoch"):
562569
unit_string = unit_string.replace("epoch", EPOCH)
563570
unit_inst = Unit(unit_string, calendar=calendar)
564-
return unit_inst.num2date(time_value)
571+
return unit_inst.num2date(
572+
time_value, only_use_cftime_datetimes=only_use_cftime_datetimes)
565573

566574

567-
def _num2date_to_nearest_second(time_value, utime):
575+
def _num2date_to_nearest_second(time_value, utime,
576+
only_use_cftime_datetimes=False):
568577
"""
569578
Return datetime encoding of numeric time value with respect to the given
570579
time reference units, with a resolution of 1 second.
@@ -574,6 +583,11 @@ def _num2date_to_nearest_second(time_value, utime):
574583
* utime (cftime.utime):
575584
cftime.utime object with which to perform the conversion/s.
576585
586+
* only_use_cftime_datetimes (bool):
587+
If True, will always return cftime datetime objects, regardless of
588+
calendar. If False, returns datetime.datetime instances where
589+
possible. Defaults to False.
590+
577591
Returns:
578592
datetime, or numpy.ndarray of datetime object.
579593
"""
@@ -582,7 +596,7 @@ def _num2date_to_nearest_second(time_value, utime):
582596
time_values = time_values.ravel()
583597

584598
# We account for the edge case where the time is in seconds and has a
585-
# half second: utime.num2date() may produce a date that would round
599+
# half second: cftime.num2date() may produce a date that would round
586600
# down.
587601
#
588602
# Note that this behaviour is different to the num2date function in version
@@ -592,7 +606,9 @@ def _num2date_to_nearest_second(time_value, utime):
592606
# later versions, if one wished to do so for the sake of consistency.
593607
has_half_seconds = np.logical_and(utime.units == 'seconds',
594608
time_values % 1. == 0.5)
595-
dates = utime.num2date(time_values)
609+
dates = cftime.num2date(
610+
time_values, utime.unit_string, calendar=utime.calendar,
611+
only_use_cftime_datetimes=only_use_cftime_datetimes)
596612
try:
597613
# We can assume all or none of the dates have a microsecond attribute
598614
microseconds = np.array([d.microsecond if d else 0 for d in dates])
@@ -603,7 +619,10 @@ def _num2date_to_nearest_second(time_value, utime):
603619
if time_values[ceil_mask].size > 0:
604620
useconds = Unit('second')
605621
second_frac = useconds.convert(0.75, utime.units)
606-
dates[ceil_mask] = utime.num2date(time_values[ceil_mask] + second_frac)
622+
dates[ceil_mask] = cftime.num2date(
623+
time_values[ceil_mask] + second_frac, utime.unit_string,
624+
calendar=utime.calendar,
625+
only_use_cftime_datetimes=only_use_cftime_datetimes)
607626
dates[round_mask] = _discard_microsecond(dates[round_mask])
608627
result = dates[0] if shape is () else dates.reshape(shape)
609628
return result
@@ -1878,7 +1897,7 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False):
18781897
raise ValueError("Unable to convert from '%r' to '%r'." %
18791898
(self, other))
18801899

1881-
def utime(self):
1900+
def utime(self, only_use_cftime_datetimes=False):
18821901
"""
18831902
Returns a cftime.utime object which performs conversions of
18841903
numeric time values to/from datetime objects given the current
@@ -1888,6 +1907,13 @@ def utime(self):
18881907
'<time-unit> since <time-origin>'
18891908
i.e. 'hours since 1970-01-01 00:00:00'
18901909
1910+
Kwargs:
1911+
1912+
* only_use_cftime_datetimes (bool):
1913+
If True, num2date method will always return cftime datetime
1914+
objects, regardless of calendar. If False, returns
1915+
datetime.datetime instances where possible. Defaults to False.
1916+
18911917
Returns:
18921918
cftime.utime.
18931919
@@ -1915,7 +1941,9 @@ def utime(self):
19151941
# ensure to strip out non-parsable 'UTC' postfix, which
19161942
# is generated by UDUNITS-2 formatted output
19171943
#
1918-
return cftime.utime(str(self).rstrip(" UTC"), self.calendar)
1944+
return cftime.utime(
1945+
str(self).rstrip(" UTC"), self.calendar,
1946+
only_use_cftime_datetimes=only_use_cftime_datetimes)
19191947

19201948
def date2num(self, date):
19211949
"""
@@ -1956,7 +1984,7 @@ def date2num(self, date):
19561984
date = _discard_microsecond(date)
19571985
return cdf_utime.date2num(date)
19581986

1959-
def num2date(self, time_value):
1987+
def num2date(self, time_value, only_use_cftime_datetimes=False):
19601988
"""
19611989
Returns a datetime-like object calculated from the numeric time
19621990
value using the current calendar and the unit time reference.
@@ -1965,8 +1993,8 @@ def num2date(self, time_value):
19651993
'<time-unit> since <time-origin>'
19661994
i.e. 'hours since 1970-01-01 00:00:00'
19671995
1968-
The datetime objects returned are 'real' Python datetime objects
1969-
if the date falls in the Gregorian calendar (i.e. the calendar
1996+
By default, the datetime objects returned are 'real' Python datetime
1997+
objects if the date falls in the Gregorian calendar (i.e. the calendar
19701998
is 'standard', 'gregorian', or 'proleptic_gregorian' and the
19711999
date is after 1582-10-15). Otherwise a 'phoney' datetime-like
19722000
object (cftime.datetime) is returned which can handle dates
@@ -1977,8 +2005,15 @@ def num2date(self, time_value):
19772005
19782006
Args:
19792007
1980-
* time_value (float): Numeric time value/s. Maximum resolution
1981-
is 1 second.
2008+
* time_value (float):
2009+
Numeric time value/s. Maximum resolution is 1 second.
2010+
2011+
Kwargs:
2012+
2013+
* only_use_cftime_datetimes (bool):
2014+
If True, will always return cftime datetime objects, regardless of
2015+
calendar. If False, returns datetime.datetime instances where
2016+
possible. Defaults to False.
19822017
19832018
Returns:
19842019
datetime, or numpy.ndarray of datetime object.
@@ -1995,5 +2030,9 @@ def num2date(self, time_value):
19952030
['1970-01-01 06:00:00', '1970-01-01 07:00:00']
19962031
19972032
"""
1998-
cdf_utime = self.utime()
1999-
return _num2date_to_nearest_second(time_value, cdf_utime)
2033+
cdf_utime = self.utime(
2034+
only_use_cftime_datetimes=only_use_cftime_datetimes)
2035+
2036+
return _num2date_to_nearest_second(
2037+
time_value, cdf_utime,
2038+
only_use_cftime_datetimes=only_use_cftime_datetimes)

cf_units/tests/integration/test__num2date_to_nearest_second.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# (C) British Crown Copyright 2016 - 2020, Met Office
1+
# (C) British Crown Copyright 2016 - 2021, Met Office
22
#
33
# This file is part of cf-units.
44
#
@@ -32,10 +32,12 @@ def setup_units(self, calendar):
3232
self.uhours = cftime.utime('hours since 1970-01-01', calendar)
3333
self.udays = cftime.utime('days since 1970-01-01', calendar)
3434

35-
def check_dates(self, nums, utimes, expected):
35+
def check_dates(self, nums, utimes, expected, only_cftime=False):
3636
for num, utime, exp in zip(nums, utimes, expected):
37-
res = _num2date_to_nearest_second(num, utime)
37+
res = _num2date_to_nearest_second(
38+
num, utime, only_use_cftime_datetimes=only_cftime)
3839
self.assertEqual(exp, res)
40+
self.assertIsInstance(res, type(exp))
3941

4042
def check_timedelta(self, nums, utimes, expected):
4143
for num, utime, exp in zip(nums, utimes, expected):
@@ -51,6 +53,7 @@ def test_scalar(self):
5153
exp = datetime.datetime(1970, 1, 1, 0, 0, 5)
5254
res = _num2date_to_nearest_second(num, utime)
5355
self.assertEqual(exp, res)
56+
self.assertIsInstance(res, datetime.datetime)
5457

5558
def test_sequence(self):
5659
utime = cftime.utime('seconds since 1970-01-01', 'gregorian')
@@ -103,6 +106,27 @@ def test_simple_gregorian(self):
103106

104107
self.check_dates(nums, utimes, expected)
105108

109+
def test_simple_gregorian_cftime_type(self):
110+
self.setup_units('gregorian')
111+
nums = [20., 40.,
112+
75., 150.,
113+
8., 16.,
114+
300., 600.]
115+
utimes = [self.useconds, self.useconds,
116+
self.uminutes, self.uminutes,
117+
self.uhours, self.uhours,
118+
self.udays, self.udays]
119+
expected = [cftime.DatetimeGregorian(1970, 1, 1, 0, 0, 20),
120+
cftime.DatetimeGregorian(1970, 1, 1, 0, 0, 40),
121+
cftime.DatetimeGregorian(1970, 1, 1, 1, 15),
122+
cftime.DatetimeGregorian(1970, 1, 1, 2, 30),
123+
cftime.DatetimeGregorian(1970, 1, 1, 8),
124+
cftime.DatetimeGregorian(1970, 1, 1, 16),
125+
cftime.DatetimeGregorian(1970, 10, 28),
126+
cftime.DatetimeGregorian(1971, 8, 24)]
127+
128+
self.check_dates(nums, utimes, expected, only_cftime=True)
129+
106130
def test_fractional_gregorian(self):
107131
self.setup_units('gregorian')
108132
nums = [5./60., 10./60.,

cf_units/tests/test_unit.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
# (C) British Crown Copyright 2010 - 2020, Met Office
2+
# (C) British Crown Copyright 2010 - 2021, Met Office
33
#
44
# This file is part of cf-units.
55
#
@@ -29,6 +29,7 @@
2929
from operator import truediv
3030

3131
import numpy as np
32+
import cftime
3233

3334
import cf_units as unit
3435
from cf_units import suppress_errors
@@ -262,7 +263,7 @@ def test_add_float_offset(self):
262263
def test_not_numerical_offset(self):
263264
u = Unit('meter')
264265
with self.assertRaisesRegex(TypeError,
265-
'unsupported operand type'):
266+
'unsupported operand type'):
266267
operator.add(u, 'not_a_number')
267268

268269
def test_unit_unknown(self):
@@ -986,7 +987,16 @@ class TestNumsAndDates(unittest.TestCase):
986987
def test_num2date(self):
987988
u = Unit('hours since 2010-11-02 12:00:00',
988989
calendar=unit.CALENDAR_STANDARD)
989-
self.assertEqual(str(u.num2date(1)), '2010-11-02 13:00:00')
990+
res = u.num2date(1)
991+
self.assertEqual(str(res), '2010-11-02 13:00:00')
992+
self.assertIsInstance(res, datetime.datetime)
993+
994+
def test_num2date_cftime_type(self):
995+
u = Unit('hours since 2010-11-02 12:00:00',
996+
calendar=unit.CALENDAR_STANDARD)
997+
res = u.num2date(1, only_use_cftime_datetimes=True)
998+
self.assertEqual(str(res), '2010-11-02 13:00:00')
999+
self.assertIsInstance(res, cftime.DatetimeGregorian)
9901000

9911001
def test_date2num(self):
9921002
u = Unit('hours since 2010-11-02 12:00:00',

0 commit comments

Comments
 (0)