Skip to content

Commit

Permalink
Merge 2c9ba56 into 3a85094
Browse files Browse the repository at this point in the history
  • Loading branch information
rcomer committed May 25, 2021
2 parents 3a85094 + 2c9ba56 commit 7b05dc4
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 139 deletions.
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)


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)


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'.
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)
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)

0 comments on commit 7b05dc4

Please sign in to comment.