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
135 changes: 74 additions & 61 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 @@ -484,7 +484,7 @@ def _discard_microsecond(date):
dates = np.asarray(date)
shape = dates.shape
dates = dates.ravel()
# Create date objects of the same type returned by utime.num2date()
# Create date objects of the same type returned by cftime.num2date()
# (either datetime.datetime or cftime.datetime), discarding the
# microseconds
dates = np.array([d and d.__class__(d.year, d.month, d.day,
Expand All @@ -494,7 +494,7 @@ def _discard_microsecond(date):
return result


def num2date(time_value, unit, calendar):
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):
unit = 'days since 001-01-01 00:00:00'}
calendar = 'proleptic_gregorian'.
trexfeathers marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -535,6 +532,13 @@ def num2date(time_value, unit, calendar):
* calendar (string):
Name of the calendar, see cf_units.CALENDARS.

Kwargs:

* 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 True.

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

Expand All @@ -561,38 +565,50 @@ def num2date(time_value, unit, calendar):
if unit_string.endswith(" since epoch"):
unit_string = unit_string.replace("epoch", EPOCH)
unit_inst = Unit(unit_string, calendar=calendar)
return unit_inst.num2date(time_value)
return unit_inst.num2date(
time_value, only_use_cftime_datetimes=only_use_cftime_datetimes)


def _num2date_to_nearest_second(time_value, utime):
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 True.

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

"""
time_values = np.asanyarray(time_value)
shape = time_values.shape
time_values = time_values.ravel()

# We account for the edge case where the time is in seconds and has a
# half second: utime.num2date() may produce a date that would round
# half second: cftime.num2date() may produce a date that would round
# down.
#
# Note that this behaviour is different to the num2date function in version
# 1.1 and earlier of cftime 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
# 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 = utime.num2date(time_values)
dates = cftime.num2date(
time_values, cftime_unit, calendar=unit.calendar,
only_use_cftime_datetimes=only_use_cftime_datetimes)
rcomer marked this conversation as resolved.
Show resolved Hide resolved
try:
# We can assume all or none of the dates have a microsecond attribute
microseconds = np.array([d.microsecond if d else 0 for d in dates])
Expand All @@ -602,8 +618,11 @@ 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)
dates[ceil_mask] = utime.num2date(time_values[ceil_mask] + second_frac)
second_frac = useconds.convert(0.75, time_units)
dates[ceil_mask] = cftime.num2date(
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)
return result
Expand Down Expand Up @@ -1807,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 @@ -1878,27 +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):
@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'

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 @@ -1915,7 +1919,7 @@ def utime(self):
# 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)
return str(self).rstrip(" UTC")

def date2num(self, date):
"""
Expand Down Expand Up @@ -1952,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):
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 @@ -1965,20 +1968,28 @@ def num2date(self, time_value):
'<time-unit> since <time-origin>'
i.e. 'hours since 1970-01-01 00:00:00'

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.

Args:

* time_value (float): Numeric time value/s. Maximum resolution
is 1 second.
* time_value (float):
Numeric time value/s. Maximum resolution is 1 second.

Kwargs:

* 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 True.

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

"""
cdf_utime = self.utime()
return _num2date_to_nearest_second(time_value, cdf_utime)

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