Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates for cftime 1.5.0 #168

Merged
merged 11 commits into from
Jun 8, 2021
Merged
116 changes: 45 additions & 71 deletions cf_units/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,14 +359,14 @@ def julian_day2date(julian_day, calendar):

>>> import cf_units
>>> import datetime
>>> dt = datetime.datetime(1970, 1, 1, 0, 0, 0)
>>> dt = cftime.datetime(1970, 1, 1, 0, 0, 0)
>>> jd = cf_units.date2julian_day(dt, cf_units.CALENDAR_STANDARD)
>>> print(cf_units.julian_day2date(jd, cf_units.CALENDAR_STANDARD))
1970-01-01 00:00:00

"""

return cftime.DateFromJulianDay(julian_day, calendar)
return cftime.datetime.fromordinal(julian_day, calendar)
rcomer marked this conversation as resolved.
Show resolved Hide resolved


def date2julian_day(date, calendar):
Expand Down Expand Up @@ -398,13 +398,13 @@ def date2julian_day(date, calendar):

>>> import cf_units
>>> import datetime
>>> cf_units.date2julian_day(datetime.datetime(1970, 1, 1, 0, 0, 0),
>>> cf_units.date2julian_day(cftime.datetime(1970, 1, 1, 0, 0, 0),
... cf_units.CALENDAR_STANDARD)
2440587.5...

"""

return cftime.JulianDayFromDate(date, calendar)
return date.toordinal(fractional=True)
rcomer marked this conversation as resolved.
Show resolved Hide resolved


def date2num(date, unit, calendar):
Expand Down Expand Up @@ -494,7 +494,7 @@ def _discard_microsecond(date):
return result


def num2date(time_value, unit, calendar, only_use_cftime_datetimes=False):
def num2date(time_value, unit, calendar, only_use_cftime_datetimes=True):
"""
Return datetime encoding of numeric time value (resolution of 1 second).

Expand All @@ -508,14 +508,11 @@ def num2date(time_value, unit, calendar, only_use_cftime_datetimes=False):
unit = 'days since 001-01-01 00:00:00'}
calendar = 'proleptic_gregorian'.
trexfeathers marked this conversation as resolved.
Show resolved Hide resolved

By default, the datetime instances returned are 'real' python datetime
objects if the date falls in the Gregorian calendar (i.e.
calendar='proleptic_gregorian', or calendar = 'standard' or 'gregorian'
and the date is after 1582-10-15). Otherwise, they are 'phony' datetime
objects which support some but not all the methods of 'real' python
datetime objects. This is because the python datetime module cannot
use the 'proleptic_gregorian' calendar, even before the switch
occured from the Julian calendar in 1582. The datetime instances
By default, the datetime instances returned are cftime.datetime objects,
regardless of calendar. If the only_use_cftime_datetimes keyword is set to
False, they are datetime.datetime objects if the date falls in the
Gregorian calendar (i.e. calendar is 'proleptic_gregorian', 'standard' or
'gregorian' and the date is after 1582-10-15). The datetime instances
do not contain a time-zone offset, even if the specified unit
contains one.

Expand All @@ -540,7 +537,7 @@ def num2date(time_value, unit, calendar, only_use_cftime_datetimes=False):
* only_use_cftime_datetimes (bool):
If True, will always return cftime datetime objects, regardless of
calendar. If False, returns datetime.datetime instances where
possible. Defaults to False.
possible. Defaults to True.

Returns:
datetime, or numpy.ndarray of datetime object.
Expand Down Expand Up @@ -572,24 +569,25 @@ def num2date(time_value, unit, calendar, only_use_cftime_datetimes=False):
time_value, only_use_cftime_datetimes=only_use_cftime_datetimes)


def _num2date_to_nearest_second(time_value, utime,
only_use_cftime_datetimes=False):
def _num2date_to_nearest_second(time_value, unit,
only_use_cftime_datetimes=True):
"""
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 (cftime.utime):
cftime.utime object with which to perform the conversion/s.
* unit (Unit):
cf_units.Unit object with which to perform the conversion/s.

* only_use_cftime_datetimes (bool):
If True, will always return cftime datetime objects, regardless of
calendar. If False, returns datetime.datetime instances where
possible. Defaults to False.
possible. Defaults to True.

Returns:
datetime, or numpy.ndarray of datetime object.

"""
time_values = np.asanyarray(time_value)
shape = time_values.shape
Expand All @@ -604,10 +602,12 @@ def _num2date_to_nearest_second(time_value, utime,
# those versions, a half-second value would be rounded up or down
# arbitrarily. It is probably not possible to replicate that behaviour with
# later versions, if one wished to do so for the sake of consistency.
has_half_seconds = np.logical_and(utime.units == 'seconds',
cftime_unit = unit.cftime_unit
time_units = cftime_unit.split(' ')[0]
has_half_seconds = np.logical_and(time_units == 'seconds',
time_values % 1. == 0.5)
dates = cftime.num2date(
time_values, utime.unit_string, calendar=utime.calendar,
time_values, cftime_unit, calendar=unit.calendar,
only_use_cftime_datetimes=only_use_cftime_datetimes)
try:
# We can assume all or none of the dates have a microsecond attribute
Expand All @@ -618,10 +618,10 @@ def _num2date_to_nearest_second(time_value, utime,
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)
second_frac = useconds.convert(0.75, time_units)
dates[ceil_mask] = cftime.num2date(
time_values[ceil_mask] + second_frac, utime.unit_string,
calendar=utime.calendar,
time_values[ceil_mask] + second_frac, cftime_unit,
calendar=unit.calendar,
only_use_cftime_datetimes=only_use_cftime_datetimes)
dates[round_mask] = _discard_microsecond(dates[round_mask])
result = dates[0] if shape is () else dates.reshape(shape)
Expand Down Expand Up @@ -1826,13 +1826,14 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False):
result = value
else:
result = copy.deepcopy(value)
# Use utime for converting reference times that are not using a
# Use cftime for converting reference times that are not using a
# gregorian calendar as it handles these and udunits does not.
if self.is_time_reference() \
and self.calendar != CALENDAR_GREGORIAN:
ut1 = self.utime()
ut2 = other.utime()
result = ut2.date2num(ut1.num2date(result))
result_datetimes = cftime.num2date(
result, self.cftime_unit, self.calendar)
result = cftime.date2num(
result_datetimes, other.cftime_unit, other.calendar)
# Preserve the datatype of the input array if it was float32.
if (isinstance(value, np.ndarray) and
value.dtype == np.float32):
Expand Down Expand Up @@ -1897,34 +1898,11 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False):
raise ValueError("Unable to convert from '%r' to '%r'." %
(self, other))

def utime(self, only_use_cftime_datetimes=False):
@property
def cftime_unit(self):
"""
Returns a cftime.utime object which performs conversions of
numeric time values to/from datetime objects given the current
calendar and unit time reference.

The current unit time reference must be of the form:
'<time-unit> since <time-origin>'
i.e. 'hours since 1970-01-01 00:00:00'

Kwargs:

* only_use_cftime_datetimes (bool):
If True, num2date method will always return cftime datetime
objects, regardless of calendar. If False, returns
datetime.datetime instances where possible. Defaults to False.

Returns:
cftime.utime.

For example:

>>> import cf_units
>>> u = cf_units.Unit('hours since 1970-01-01 00:00:00',
... calendar=cf_units.CALENDAR_STANDARD)
>>> ut = u.utime()
>>> print(ut.num2date(2))
1970-01-01 02:00:00
Returns a string suitable for passing as a unit to cftime.num2date and
cftime.date2num.

"""
if self.calendar is None:
Expand All @@ -1941,9 +1919,7 @@ def utime(self, only_use_cftime_datetimes=False):
# ensure to strip out non-parsable 'UTC' postfix, which
# is generated by UDUNITS-2 formatted output
#
return cftime.utime(
str(self).rstrip(" UTC"), self.calendar,
only_use_cftime_datetimes=only_use_cftime_datetimes)
return str(self).rstrip(" UTC")

def date2num(self, date):
"""
Expand Down Expand Up @@ -1980,11 +1956,10 @@ def date2num(self, date):

"""

cdf_utime = self.utime()
date = _discard_microsecond(date)
return cdf_utime.date2num(date)
return cftime.date2num(date, self.cftime_unit, self.calendar)

def num2date(self, time_value, only_use_cftime_datetimes=False):
def num2date(self, time_value, only_use_cftime_datetimes=True):
"""
Returns a datetime-like object calculated from the numeric time
value using the current calendar and the unit time reference.
Expand All @@ -1993,12 +1968,13 @@ def num2date(self, time_value, only_use_cftime_datetimes=False):
'<time-unit> since <time-origin>'
i.e. 'hours since 1970-01-01 00:00:00'

By default, the datetime objects returned are 'real' Python datetime
objects if the date falls in the Gregorian calendar (i.e. the calendar
is 'standard', 'gregorian', or 'proleptic_gregorian' and the
date is after 1582-10-15). Otherwise a 'phoney' datetime-like
object (cftime.datetime) is returned which can handle dates
that don't exist in the Proleptic Gregorian calendar.
By default, the datetime instances returned are cftime.datetime
objects, regardless of calendar. If the only_use_cftime_datetimes
keyword is set to False, they are datetime.datetime objects if the date
falls in the Gregorian calendar (i.e. calendar is
'proleptic_gregorian', 'standard' or gregorian' and the date is after
1582-10-15). The datetime instances 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.
Expand All @@ -2013,7 +1989,7 @@ def num2date(self, time_value, only_use_cftime_datetimes=False):
* only_use_cftime_datetimes (bool):
If True, will always return cftime datetime objects, regardless of
calendar. If False, returns datetime.datetime instances where
possible. Defaults to False.
possible. Defaults to True.

Returns:
datetime, or numpy.ndarray of datetime object.
Expand All @@ -2030,9 +2006,7 @@ def num2date(self, time_value, only_use_cftime_datetimes=False):
['1970-01-01 06:00:00', '1970-01-01 07:00:00']

"""
cdf_utime = self.utime(
only_use_cftime_datetimes=only_use_cftime_datetimes)

return _num2date_to_nearest_second(
time_value, cdf_utime,
time_value, self,
only_use_cftime_datetimes=only_use_cftime_datetimes)
Loading